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
133: Existential Types in Scala 2
I've seen a lot of developers hit a wall when they try to create collections of generic objects. You've got a generic class—say, a Repository[T]—and you want a list of repositories for different entities: one for User, one for Order, and one for Product. Naturally, you try to put them in a List[Repository[_]]. This is where you first encounter existential types in Scala 2, and it's usually where the frustration begins.
The frustration of the wildcard
When you use the underscore in Repository[_], you're telling the compiler: "I have a repository of some type, but I'm not telling you which one." In formal terms, this is an existential type. It essentially says "there exists some type T such that this is a Repository of T."
trait Repository[T] { def save(item: T): Unit def find(id: String): T } class UserRepository extends Repository[User] { def save(item: User): Unit = println(s"Saving user ${item.name}") def find(id: String): User = new User(id, "John Doe") } class OrderRepository extends Repository[Order] { def save(item: Order): Unit = println(s"Saving order ${item.orderId}") def find(id: String): Order = new Order(id, 100.0) } val repos: List[Repository[_]] = List(new UserRepository, new OrderRepository)At first glance, this looks fine. But the moment you try to actually use a repository from that list, you'll run into a wall. If you try to call
repos.head.save(someUser), the compiler will stop you. Why? Because the compiler knowsrepos.headis aRepositoryof some type, but it has no guarantee that the "some type" isUser. It could beOrder. To the compiler, you're trying to pass aUserinto a function that expects a type it can't identify.Anchoring types to instances
The "better" way to handle this in Scala 2—when you need to group these objects but still maintain a relationship between the container and the item—is to move the type from a class parameter to an abstract type member. This shifts the perspective from "this class is parameterized by T" to "this instance has a specific type associated with it."
trait Store { type Entity def save(item: Entity): Unit def find(id: String): Entity } class UserStore extends Store { type Entity = User def save(item: User): Unit = println(s"Saving user ${item.name}") def find(id: String): User = new User(id, "John Doe") } class OrderStore extends Store { type Entity = Order def save(item: Order): Unit = println(s"Saving order ${item.orderId}") def find(id: String): Order = new Order(id, 100.0) } val stores: List[Store] = List(new UserStore, new OrderStore)Now we have a
List[Store]. We've effectively hidden the type inside the trait. But wait, you might think, "Doesn't this have the same problem as the wildcard?" Not exactly. The magic here is path-dependent types. The typeEntityis now tied to the specific instance of theStore.If you have a reference to a specific store, you can use its own
Entitytype to make the compiler happy. You can't just pass a randomUserintostores.head.save(), but you can take the result ofstores.head.find("123")and pass it right back intostores.head.save(). The compiler knows that the type returned byfindis the exact same existential type required bysavefor that specific instance.The cost of losing the generic
So why not always do this? There's a trade-off. When you move from
Repository[T]to aStorewith atype Entity, you lose the ability to constrain the type from the outside. WithRepository[User], I can tell the world exactly what this repository handles. WithStore, I'm essentially saying "this handles something," and you have to interact with the instance to figure out what that something is.I generally recommend using generics (type parameters) for your primary API and logic. Use abstract type members (existentials) when you're building heterogeneous collections or plugin architectures where you need to treat different implementations uniformly, but still need those implementations to remain internally consistent with their own types.
📋 Practical Task
Implement a Heterogeneous Plugin Registry
You are building a plugin system where each plugin has its own unique Configuration type. You need to be able to store a list of plugins and update their configurations without knowing the specific configuration type of each plugin at compile time.
Requirements:
- Create a trait
Pluginthat uses an abstract type membertype Config. - The
Plugintrait should have a methodapplyConfig(config: Config): Unitand a methodgetCurrentConfig: Config. - Implement two plugins:
DatabasePlugin(using aDbConfigcase class) andApiPlugin(using anApiConfigcase class). - Create a
List[Plugin]containing both. - Write a function
refreshPlugins(plugins: List[Plugin])that iterates through the list, retrieves the current config for each plugin, and immediately passes that same config back intoapplyConfigto "reset" it.
Hint: The compiler will only allow the refreshPlugins logic if you use the return value of getCurrentConfig directly as the argument for applyConfig on the same instance.
There are no comments for now.