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
114: Common Scala Interview Questions on Variance
Variance is usually the part of the Scala interview where candidates start sweating. It's a bit abstract, and if you haven't had to design a complex library, you might feel like it's just academic noise. But in practice, it's all about one thing: type safety when dealing with hierarchies.
Why can't I just make everything covariant?
It's tempting to just slap a +T on every generic class to make your life easier. After all, if a Dog is an Animal, it feels natural that a List[Dog] should be a List[Animal]. That's covariance.
The problem is that covariance is only safe for producers. If your class also consumes values (meaning it has a method that takes T as an argument), you've just created a hole in the type system. Look at this:
case class Box[+T](value: T) {
// This wouldn't actually compile in Scala!
def put(item: T): Unit = { println(s"Putting $item in the box") }
}
If Scala allowed the put method above, I could take a Box[Dog], cast it to a Box[Animal] (which covariance allows), and then try to put a Cat into it. Your Box[Dog] now contains a Cat, and the next time you try to call a dog-specific method on that value, the JVM will crash. I've seen this mistake in countless PRs; just remember: if you're writing to it, you can't be covariant.
Where do I actually use contravariance in a real project?
Contravariance (-T) is the mirror image of covariance, and it's much rarer, which is why people struggle with it. While covariance is for producers, contravariance is for consumers.
The classic example is a Comparator or a Handler. Imagine you have a hierarchy of Animal > Dog > Pug. If you have a Handler[Animal], that handler knows how to process any animal. Therefore, it is perfectly capable of handling a Dog. In this world, the Handler[Animal] is actually a subtype of Handler[Dog].
trait Handler[-T] {
def handle(item: T): Unit
}
val animalHandler: Handler[Animal] = new Handler[Animal] {
def handle(item: Animal): Unit = println(s"Handling animal: ${item}")
}
// This works because of -T
val dogHandler: Handler[Dog] = animalHandler
If you make Handler invariant, the line val dogHandler: Handler[Dog] = animalHandler would fail. By using -T, you're telling the compiler: "This class consumes T, so a handler for a general type can always stand in for a handler of a specific type."
What's the deal with those weird lower bounds in covariant classes?
You've probably noticed that in the Scala standard library, methods on covariant classes often look like this: def cons[U >: T](that: U): List[U]. That U >: T is a lower bound, and it's the "escape hatch" that allows covariant classes to still have methods that consume values.
Since we can't use T directly as a parameter in a +T class, we introduce a new type parameter U that is a supertype of T. I like to think of it as the compiler saying, "I can't let you put a T in here because T is too flexible, but if you tell me the exact common supertype you're aiming for, we can make it work."
Here is how that looks in a simplified version of a Box:
case class Box[+T](value: T) {
// We can't use 'def put(item: T)', so we use a lower bound
def put[U >: T](item: U): Box[U] = Box(item)
}
val dogBox = Box("Fido") // Box[String]
val animalBox = dogBox.put("Generic Animal") // Now it's a Box[Any] (or whatever the common supertype is)
It's a bit of a brain-bender at first, but once you realize it's just a way to preserve type safety while maintaining flexibility, it clicks. You aren't changing the box; you're evolving the box's type to accommodate the new, broader value.
📋 Practical Task
Implementing a Variance-Safe Event Processor
You are building an event-driven system for a zoo. You have a class hierarchy: Event (base), AnimalEvent (extends Event), and FeedingEvent (extends AnimalEvent).
Your task is to implement the following:
- Create the
Eventhierarchy described above. - Create a
Processor[-T]trait with a methodprocess(event: T): Unit. Ensure it is contravariant so that aProcessor[Event]can be used wherever aProcessor[FeedingEvent]is expected. - Create a
EventBuffer[+T]case class that holds aList[T]. - Inside
EventBuffer, implement a methodaddEvent[U >: T](event: U): EventBuffer[U]that returns a new buffer containing the added event, using a lower bound to maintain covariance. - In a main method, demonstrate that you can pass a
Processor[Event]into a function that expects aProcessor[FeedingEvent].
There are no comments for now.