Scala Variance
Variance describes how a generic type Box[A] relates to Box[B] when A is a subtype of B. Should a Box[Dog] be usable where a Box[Animal] is expected? The answer depends on how the type parameter is used — and Scala gives you three options: invariant, covariant, and contravariant.
The Problem Without Variance
class Animal
class Dog extends Animal
// Without variance annotation — invariant by default
class Box[A](val item: A)
val dogBox: Box[Dog] = Box(new Dog())
// val animalBox: Box[Animal] = dogBox // Error! Box[Dog] is NOT Box[Animal]
The Three Variance Types
class Box[A] // Invariant: Box[Dog] ≠ Box[Animal]
class Box[+A] // Covariant: Box[Dog] IS-A Box[Animal] (+ means "with subtypes")
class Box[-A] // Contravariant: Box[Animal] IS-A Box[Dog] (- means "against subtypes")
Invariance — Default
class Container[A](var item: A)
val dogContainer: Container[Dog] = Container(new Dog())
// val animalContainer: Container[Animal] = dogContainer // Error
// val dogContainer2: Container[Dog] = Container(new Animal()) // Error
// Must be exactly the right type
def processAnimal(c: Container[Animal]): Unit = println("Processing animal")
// processAnimal(dogContainer) // Error — invariant
Covariance — [+A]
Covariant means "if Dog is a subtype of Animal, then Box[Dog] is a subtype of Box[Animal]." Use covariance on read-only (producer) types:
class ReadBox[+A](val item: A)
val dogBox: ReadBox[Dog] = ReadBox(new Dog())
val animalBox: ReadBox[Animal] = dogBox // OK! Dog is Animal, ReadBox[Dog] is ReadBox[Animal]
def showAnimal(box: ReadBox[Animal]): Unit =
println(s"Got: ${box.item.getClass.getSimpleName}")
showAnimal(dogBox) // Got: Dog — works!
showAnimal(ReadBox(new Animal())) // Got: Animal
Animal
└── Dog
ReadBox[Animal]
└── ReadBox[Dog] ← covariance mirrors the hierarchy
Scala's immutable List[+A] is covariant. A List[Dog] can be used anywhere a List[Animal] is expected.
Contravariance — [-A]
Contravariant means the relationship is reversed. A Printer[Animal] can be used where a Printer[Dog] is expected — because a printer that handles any animal can certainly handle a dog:
class Printer[-A]:
def print(item: A): Unit = println(item.getClass.getSimpleName)
val animalPrinter: Printer[Animal] = new Printer[Animal]
val dogPrinter: Printer[Dog] = animalPrinter // OK! Animal printer works for Dog too
def printDog(p: Printer[Dog], dog: Dog): Unit = p.print(dog)
printDog(animalPrinter, new Dog()) // Dog
printDog(dogPrinter, new Dog()) // Dog
Animal
└── Dog
Printer[Dog]
└── Printer[Animal] ← contravariance reverses the hierarchy
Scala's Function1[-A, +B] uses both: contravariant in the input type and covariant in the output type — a function that accepts any Animal can be used where a function accepting a Dog is needed.
Covariant Restriction: No var fields
Covariant type parameters cannot appear in mutable (write) positions. This prevents unsound assignments:
// This would be UNSAFE — Scala forbids it:
class BadBox[+A](var item: A) // Error: covariant type A in contravariant position
// Why unsafe? Imagine this scenario (hypothetical):
val dogBox: BadBox[Dog] = BadBox(new Dog())
val animalBox: BadBox[Animal] = dogBox // covariance allows this
animalBox.item = new Cat() // now dogBox contains a Cat! — bug!
The Variance Decision Guide
Question Variance
─────────────────────────────────────── ────────────────────────
Does A only appear in output position? Covariant [+A]
(read-only, producer)
Does A only appear in input position? Contravariant [-A]
(write-only, consumer)
Does A appear in both input and output? Invariant [A]
(read and write, mutable)
Real Example: Function Variance
// Function1[-A, +B]: contravariant input, covariant output
val animalToString: Animal => String = a => a.getClass.getSimpleName
val dogToString: Dog => String = animalToString // OK! Animal => can be used as Dog =>
// A function that handles any animal also handles dogs
def formatDog(f: Dog => String): String = f(new Dog())
println(formatDog(animalToString)) // Dog
Summary Diagram
Animal
│
Dog
Invariant Box[Animal] ←→ Box[Dog] (unrelated — no substitution)
Covariant Box[Animal] (Box[Dog] fits here)
└── Box[Dog]
Contravariant Box[Dog] (Box[Animal] fits here)
└── Box[Animal]
Variance annotations make your generic types safe and flexible. The compiler enforces the rules at compile time, so runtime type errors from variance violations are impossible in well-typed Scala code.
