Skip to content
Variance: +T, -T
step 1/5

Reading — step 1 of 5

Learn

~1 min readType System Power

Variance decides how generic types relate when their parameters relate. List[Dog] should be a List[Animal] — but is it? Depends on variance.

Three modes:

  • class Container[T]invariant: Container[Dog] is NOT a Container[Animal]
  • class Container[+T]covariant: Container[Dog] IS a Container[Animal] (subtypes flow with subtypes)
  • class Container[-T]contravariant: Container[Animal] IS a Container[Dog] (subtypes flow against)
class Animal
class Dog extends Animal

class Box[T](val item: T)              // invariant
class ReadOnlyBox[+T](val item: T)     // covariant
class Sink[-T](val accept: T => Unit)   // contravariant

Why variance matters:

val dogs: ReadOnlyBox[Dog] = new ReadOnlyBox(new Dog)
val animals: ReadOnlyBox[Animal] = dogs    // works because + means covariant

val invariantDogs: Box[Dog] = new Box(new Dog)
// val invariantAnimals: Box[Animal] = invariantDogs   // ERROR — invariant

The rule:

  • If T appears only as output (return type, val): make it covariant (+T)
  • If T appears only as input (parameter): make it contravariant (-T)
  • If T appears as both: must stay invariant

This is PECS for Scala (similar to Java's wildcards).

Function1[-A, +R] — Scala's function type:

  • Argument A is contravariant (a Function1[Animal, ...] works where Function1[Dog, ...] is expected)
  • Result R is covariant

Practical use:

sealed trait Result[+T]
case class Ok[+T](value: T) extends Result[T]
case object Err extends Result[Nothing]    // Nothing is bottom — subtype of everything

val r: Result[Int] = Err     // works because Result is covariant + Nothing <: Int

This is how None, Nil work — they're Option[Nothing] / List[Nothing] and fit anywhere a Option[T] / List[T] is expected.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…