Skip to Content
Course content

131: Path-Dependent Types

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've been thinking about how to handle identity in a game engine lately. Specifically, the problem of "leaking" objects between different game sessions. Imagine you have two separate matches running in the same JVM. You definitely don't want a Player object from Match A to accidentally be passed into a method that's managing Match B. It would be a nightmare to debug.

Trying to isolate players

My first instinct was to just nest the Player class inside the Game class. It seems intuitive—the player belongs to the game, so the class should live there. Let's see how the compiler handles this:

class Game(val gameId: String) {
  class Player(val name: String)
}

val game1 = new Game("Match-1")
val game2 = new Game("Match-2")

val player1 = new game1.Player("Alice")
val player2 = new game2.Player("Bob")

This looks fine so far. I'm instantiating Player using the specific instance of the game (game1.Player). But here is where things get interesting. I tried to write a simple method to swap players, or maybe just move one to a list, and I hit a wall.

def movePlayer(p: game1.Player) = {
  println(s"Moving ${p.name}")
}

movePlayer(player1) // Works great.
movePlayer(player2) // Compiler error!

The compiler tells me it found game2.Player but expected game1.Player. Now, if you're coming from Java or C#, this feels completely wrong. Player is the same class definition in both cases. Why on earth does the compiler think they are different types?

The "Path" in Path-Dependent Types

This is the core of path-dependent types. In Scala, when you define a class inside another class, the inner class's type is tied to the specific instance of the outer class.

The "path" to the type player1 is game1.Player. The "path" to player2 is game2.Player. Because game1 and game2 are different objects, game1.Player and game2.Player are treated as entirely different types by the compiler. It's a powerful way to enforce a strict relationship at the type level. I don't have to write a runtime check like if (player.gameId != this.gameId) throw Exception(...); the code simply won't compile if I try to mix them up.

Breaking the lock

But this creates a problem. What if I want to write a method that can take a player from any game? If I hardcode game1.Player, I'm stuck. I tried using a generic type, but that feels like overkill for a simple relationship.

The trick here is to make the method depend on the same instance as the player. I have to pass the game instance in as well, so the compiler can verify the "path" matches:

def movePlayer(game: Game, p: game.Player) = {
  println(s"Moving ${p.name} within ${game.gameId}")
}

movePlayer(game1, player1) // Works.
movePlayer(game2, player2) // Works.
movePlayer(game1, player2) // Still fails! (As it should).

By using game: Game and p: game.Player, I've told Scala: "I don't care which game this is, as long as the player belongs to the specific game instance provided in the first argument."

It's a bit of a mind-shift. You stop thinking about Player as a global type and start thinking about it as a type that only exists in the context of a specific Game. It's restrictive, yes, but it's the kind of restriction that saves you from a 3:00 AM production crash because a pointer leaked across a session boundary.




📋 Practical Task

Implementing a Secure Vault System

You are building a security system where Keys are strictly bound to the Vault that created them. A key from Vault A must never be accepted by Vault B.

Requirements:

  • Create a class Vault that takes a vaultId: String.
  • Inside Vault, define a class Key that takes a keyCode: String.
  • Implement a method inside Vault called unlock(key: this.Key) that prints "Vault [id] opened with key [code]".
  • In your main application:
    • Create two different Vault instances.
    • Create a Key for the first vault.
    • Demonstrate that calling unlock on the first vault with its own key works.
    • Try (and comment out) the code that attempts to use the first vault's key to unlock the second vault, observing the compiler error.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.