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.

Leave a Comment

Your email address will not be published. Required fields are marked *