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
29: Variance Annotations
Imagine you're running a shipping warehouse. You have boxes. If I tell you, "I need a box of Animals," and you hand me a box full of Golden Retrievers, I'm happy. Why? Because every Golden Retriever is an Animal. The "box-ness" follows the direction of the inheritance. That's covariance.
Now, imagine you're hiring a veterinarian. You tell the agency, "I need someone who can treat Golden Retrievers." The agency sends you a vet who is qualified to treat any animal in the kingdom. Are you okay with that? Absolutely. The vet's capability is broader than what you required. But notice the flip: the requirement was specific (Dogs), but the provider was general (Animals). That's contravariance.
Finally, imagine a specialized grooming machine that both takes a dog in and outputs a groomed dog. If you put a cat in, it breaks. If you try to treat it as a general "Animal Machine," you might accidentally put a cat in. To keep things safe, the machine must be exactly what it says it is. That's invariance.
Riding the Wave with Covariance (+T)
In Scala, we use the + symbol to mark a type parameter as covariant. This tells the compiler: "If Dog is a subtype of Animal, then Box[Dog] is a subtype of Box[Animal]."
trait Animal
class Dog extends Animal
class Cat extends Animal
// The '+' makes this covariant
case class Box[+T](item: T)
val dogBox: Box[Dog] = Box(new Dog)
val animalBox: Box[Animal] = dogBox // This works!
I'll give you a warning here: covariance comes with a restriction. You can only use T in "out" positions (return types). You cannot use a covariant T as a method argument. Why? Because if Box[+T] allowed a put(item: T) method, I could take my Box[Dog], treat it as a Box[Animal], and then try to put a Cat into it. That would crash the program at runtime, so Scala stops you at compile time.
Flipping the Script with Contravariance (-T)
Contravariance is a bit more brain-bending because it moves in the opposite direction of the class hierarchy. We use the - symbol. This is most common when we are dealing with "consumers" or "handlers"—things that take a value and do something with it.
trait Animal
class Dog extends Animal
// The '-' makes this contravariant
trait Vet[-T] {
def treat(patient: T): Unit
}
val generalVet: Vet[Animal] = new Vet[Animal] {
def treat(patient: Animal): Unit = println("Treating an animal...")
}
// A Vet who can treat any Animal can certainly treat a Dog
val dogVet: Vet[Dog] = generalVet
Notice that the assignment is the opposite of the Box example. Here, the Vet[Animal] is a subtype of Vet[Dog]. In contravariance, T can only appear in "in" positions (method arguments). You can't have a method that returns T in a contravariant class, because you can't guarantee that a general animal vet will return a specific dog.
The Strict Middle Ground: Invariance
If you don't put a + or a -, the type is invariant. This is the default. It means List[Dog] is not a List[Animal], and List[Animal] is not a List[Dog]. They are completely different types.
You need this whenever your class both produces and consumes the type. The most common example is a mutable collection. If you could treat a MutableList[Dog] as a MutableList[Animal], you could add a Cat to it, and suddenly your "Dog List" contains a cat. To prevent this heap of chaos, Scala forces the types to match exactly.
class MutableBox[T](var item: T)
val dogBox = new MutableBox(new Dog)
// val animalBox: MutableBox[Animal] = dogBox // COMPILE ERROR!
When you're designing your own generics, always ask yourself: "Is this a producer, a consumer, or both?" If it's a producer, go with +T. If it's a consumer, go with -T. If it's both, stick with the default invariance.
📋 Practical Task
Refining the Pet Care Provider Hierarchy
You are building a system for a veterinary clinic. You have a hierarchy of animals (Animal → Dog → Puppy). You need to implement two generic components: a Kennel (which holds animals) and a CareProvider (which treats animals).
Your goal: Fix the variance annotations in the following code so that the logic follows the rules we discussed. Specifically:
- A
Kennel[Puppy]should be usable wherever aKennel[Animal]is expected. - A
CareProvider[Animal]should be usable wherever aCareProvider[Puppy]is expected.
trait Animal
class Dog extends Animal
class Puppy extends Dog
// TODO: Add the correct variance annotation to T
case class Kennel[T](resident: T)
// TODO: Add the correct variance annotation to T
trait CareProvider[T] {
def provideCare(patient: T): Unit
}
object VarianceExercise extends App {
val puppyKennel = Kennel(new Puppy)
val animalKennel: Kennel[Animal] = puppyKennel // Should compile
val generalProvider = new CareProvider[Animal] {
def provideCare(patient: Animal): Unit = println("Providing general care")
}
val puppyProvider: CareProvider[Puppy] = generalProvider // Should compile
println("Variance settings are correct!")
}
There are no comments for now.