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.