Scala
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Scala
-
Section 4: Functional Scala
-
Section 5: Collections in Depth
-
Section 6: Type System
-
Section 7: Concurrency and Ecosystem
-
Section 8: Practical Projects
-
Section 9: Interview Practice
-
Section 10: Data Structures and Algorithms in Scala
-
Section 11: More Practice Exercises
-
Section 12: Advanced Functional Patterns
-
Section 13: More Ecosystem
-
Section 14: Scala Collections Library Deep Dive
-
Section 15: Scala Standard Library Deep Dive
-
Section 16: Akka Ecosystem Deep Dive
-
Section 17: Cats and Cats Effect Deep Dive
-
Section 18: Play Framework Deep Dive
-
Section 19: Apache Spark with Scala Deep Dive
-
Section 20: Scala Build Tools Deep Dive
-
Section 21: Scala 3 Specific Features
-
90: Union and Intersection Types
-
Section 22: Scala Testing Deep Dive
-
Section 23: Functional Domain Modeling
-
Section 24: More Data Structures and Algorithms in Scala
-
Section 25: Scala for Data Engineering
-
Section 26: More Practical Projects
-
Section 27: More Interview and Review
-
Section 28: ZIO Ecosystem Deep Dive
-
Section 29: Scala for Machine Learning
-
Section 30: Scala Microservices Architecture
-
Section 31: Scala Type System Deep Dive
-
Section 32: More Practice and Drills
-
Section 33: Scala Performance Deep Dive
-
Section 34: Scala Ecosystem Tooling
-
Section 35: Scala for Reactive Systems
-
Section 36: More Real-World Case Studies
-
Section 37: Scala for Financial Systems
-
Section 38: Scala GraphQL and gRPC
-
Section 39: More Final Projects
-
Section 40: More Interview and Final Review
-
Section 41: Scala for Streaming Data
-
Section 42: Scala Security Practices
-
Section 43: More Language Deep Dive
-
Section 44: Scala Command-Line Tools
-
Section 45: Scala Documentation and Style
-
Section 46: Scala Dependency Management
-
Section 47: More Practical Backend Patterns
-
Section 48: Scala for Event-Driven Architecture
-
Section 49: More Practice Drills Round 2
-
Section 50: Scala Compiler Deep Dive
-
Section 51: Scala for Web Frontends
-
Section 52: More Data Engineering Practice
-
Section 53: Scala Observability
-
Section 54: More Advanced Practice Projects
-
Section 55: Scala for Legacy Java Integration
-
Section 56: More Testing Practice
-
Section 57: Final Mastery Review
-
Section 58: Scala History and Ecosystem Context
-
Section 59: More Concurrency Patterns
-
Section 60: Scala for Configuration Management
-
Section 61: More Domain Modeling Practice
131: Path-Dependent Types
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
Vaultthat takes avaultId: String. - Inside
Vault, define a classKeythat takes akeyCode: String. - Implement a method inside
Vaultcalledunlock(key: this.Key)that prints "Vault [id] opened with key [code]". - In your main application:
- Create two different
Vaultinstances. - Create a
Keyfor the first vault. - Demonstrate that calling
unlockon 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.
- Create two different
There are no comments for now.