Variance determines when a container of a subtype can be used as a container of a supertype.
Classes, traits and methods take type parameters: class Box[A](value: A), def first[A](xs: List[A]): Option[A]. The compiler infers them at call sites.
Variance describes how subtyping of the parameter relates to subtyping of the generic type, given Cat <: Animal:
- Covariant
+A:List[Cat] <: List[Animal]. Right for immutable producers:List,Option,Vector,Either. - Contravariant
-A:Printer[Animal] <: Printer[Cat]. Right for consumers: a function that accepts anyAnimalcan be used where one acceptingCatis needed.Function1[-A, +B]is contravariant in input and covariant in output. - Invariant (no annotation): no relationship. Required for mutable containers:
Array[Cat]must not be anArray[Animal], or you could put aDoginto it.
The compiler checks variance positions: a covariant type can't appear as a method parameter. The fix is a lower bound: def prepend[B >: A](b: B): List[B] widens the element type as needed, which is how List lets you add an Animal to a List[Cat].
Upper bounds A <: Comparable[A] restrict a parameter to subtypes. Prefer type classes (using Ordering[A]) for capabilities like ordering, because they work for types you don't control.
Type parameters are erased at runtime on the JVM: List[Int] and List[String] are the same class, so case xs: List[Int] can't be checked. A ClassTag or TypeTest recovers some information when needed.
Example
sealed trait Animal { def name: String }
case class Cat(name: String) extends Animal
case class Dog(name: String) extends Animal
final case class Box[+A](value: A):
def map[B](f: A => B): Box[B] = Box(f(value))
def orElse[B >: A](other: B): List[B] = List(value, other) // lower bound
trait Printer[-A]:
def print(a: A): String
val animalPrinter: Printer[Animal] = a => s"animal ${a.name}"
val catPrinter: Printer[Cat] = animalPrinter // OK: contravariance
val catBox: Box[Cat] = Box(Cat("Tom"))
val animalBox: Box[Animal] = catBox // OK: covariance
println(animalBox.map(_.name)) // Box(Tom)
println(catBox.orElse(Dog("Rex"))) // List(Cat(Tom), Dog(Rex))
println(catPrinter.print(Cat("Kit"))) // animal Kit
def maxOf[A <: Comparable[A]](a: A, b: A): A = if a.compareTo(b) >= 0 then a else b
println(maxOf("pear", "apple")) // pear
// val arr: Array[Animal] = Array[Cat]() // error: Array is invariantRemember
+Acovariant for producers,-Acontravariant for consumers, invariant for mutable- Use a lower bound
B >: Ato accept wider types in covariant classes - Upper bounds
A <: Trestrict; type classes are often better - Generics are erased at runtime
Watch out for
- Matching
case xs: List[Int]and expecting the element type to be checked; it's erased.