Skip to content
AgileLogics
5 min read

Variance

Prerequisites: Generics

If Cat is a subtype of Animal, is List[Cat] a subtype of List[Animal]? Variance annotations on type parameters answer that.

Declaration Name F[Cat] vs F[Animal]
F[+A] covariant F[Cat] is an F[Animal]
F[-A] contravariant F[Animal] is an F[Cat]
F[A] invariant unrelated

Covariance: producers

trait Animal:
  def name: String
case class Cat(name: String) extends Animal
case class Dog(name: String) extends Animal

case class Box[+A](content: A)

def names(animals: List[Animal]): String = animals.map(_.name).mkString(", ")
def open(box: Box[Animal]): String = box.content.name

@main def covariance(): Unit =
  val cats: List[Cat] = List(Cat("Tom"), Cat("Felix"))
  println(names(cats))
  println(open(Box(Dog("Rex"))))
  val mixed = Cat("Tom") :: List(Dog("Rex"))
  println(mixed)
Tom, Felix
Rex
List(Cat(Tom), Dog(Rex))

List and Option are covariant (List[+A]), so a List[Cat] can be used wherever a List[Animal] is expected. That's safe because they're immutable: you can only read As out of them. Adding a Dog to a list of cats gives a List[Animal], never a corrupted List[Cat].

Contravariance: consumers

trait Animal:
  def name: String
case class Cat(name: String) extends Animal

trait Printer[-A]:
  def print(value: A): String

val animalPrinter: Printer[Animal] = a => s"Animal: ${a.name}"

def printCat(cat: Cat, printer: Printer[Cat]): String = printer.print(cat)

@main def contravariance(): Unit =
  println(printCat(Cat("Tom"), animalPrinter))
Animal: Tom

Something that can print any animal can certainly print a cat, so a Printer[Animal] works as a Printer[Cat]. Function parameters are contravariant for the same reason: Function1[-A, +B].

Why mutable collections are invariant

import scala.collection.mutable.ArrayBuffer

trait Animal
case class Cat(name: String) extends Animal

@main def invariance(): Unit =
  val cats = ArrayBuffer(Cat("Tom"))
  val animals: ArrayBuffer[Animal] = cats
-- [E007] Type Mismatch Error: Main.scala:8:37 ---------------------------------
8 |  val animals: ArrayBuffer[Animal] = cats
  |                                     ^^^^
  |              Found:    (cats : scala.collection.mutable.ArrayBuffer[Cat])
  |              Required: scala.collection.mutable.ArrayBuffer[Animal]
  |
  | longer explanation available when compiling with `-explain`
1 error found

If this compiled, you could add a Dog through animals and break cats. The rule of thumb: types that only produce A can be covariant, types that only consume A can be contravariant, and types that do both must be invariant.