Variance

The variance in Scala aims to provide flexibility to the inheritance on parametric types.

Two examples where it helps are:

  • Make List[Dog] a subtype of List[Animal]
  • Reuse a function Animal=>Boolean as a function Dog=>Boolean

A concrete example

Consider the following example:

// Our pet classes
sealed class Animal
class Dog extends Animal
class Cat extends Animal

Let’s say you work for a veterinary. You’re writing an API.

You want to modularize the functions that retrieve pets’ information from a DB, like:

  • getName
  • getBreed
  • etc.

Then, we could define a class Func that will encapsulate an information retriever function, f.

Here is our first attempt:

class Func[I,O] (val f: I => O) {
  def apply(i: I): O = f(i)
}

Good! We can define our first instance of Func:

// Our first information retriever
// Is my animal a dog?
val isADog: Func[Animal, Boolean] = {
  new Func((i: Animal) => i.isInstanceOf[Dog])
}

We say that Fun is invariant in I (Animal) and invariant in O (Boolean), as there is not subtype association done by the compiler.

Generalizing Func: Covariance

The problem

Let’s say you handle many Func implementations:

val x:  Func[Animal, Boolean] = ...
val y:  Func[Animal, String] = ...
val z:  Func[Animal, Int] = ...

It would be good to be able to treat all x, y and z polymorphically.

For instance be able to do:

val l: List[Func[Animal, AnyVal]] =
   List(x, y, z) // won't work, invariance

Our initial declaration of Func[I, O] was invariant in both I and O. It does not allow this supertype relation.

The solution

The solution is covariance.

The principle: making Clz covariant in A means that

  • Cat <: Animal implies
  • Clz[Cat] <: Clz[Animal]

In other words: the inheritance of this parametric type follows the one from the parameter type.

Back to our example, we simply re-define Func, but making it covariant in O:

// now covariant on O
class Func[I, +O] (val f: I => O) {
  def apply(i: I): O = f(i)
}
// specific type
val isADog: Func[Animal, Boolean] = ...

// generic type
val covIsDogForAnyVal: Func[Animal, AnyVal] =
        isADog   // assigned to a more general type
                 // works because
                 // Boolean <: AnyVal,
                 // and thanks to covariance
                 // Func[X, Boolean] <: Func[X, AnyVal]

Specializing Func: Contravariance

The problem

Let’s say we have our function Func[Animal, Boolean]. Given that Dog <: Animal (Dog is a subtype of Animal), it seems natural to be able to apply such function to a Dog too.

The solution

The solution is contravariance.

The principle: making Clz contravariant in A means that:

  • Cat <: Animal implies
  • Clz[Cat] >: Clz[Animal]

In other words the inheritance of this parametric type follows inversely the one from the parameter type.

We simply redefine Func but making it contravariant in I this time:

// now contravariant in I
class Func[-I, O] (val f: I => O) {
  def apply(i: I): O = f(i)
}
// generic type
val isDog: Func[Animal, Boolean] =
        new Func((i: Animal) => i.isInstanceOf[Dog])

// specific type
val contrvarIsDog: Func[Dog, Boolean] =
        isDog // assigned to a more specific type
              // works because
              // Dog <: Animal, and thanks
              // to contravariance
              // Func[Dog, X] >: Func[Animal, X]

Use in the Scala library

I invite you to take a look at the implementation of the trait Function2 in Scala v2.12.

See its declaration (ignore the @specialized annotation):

trait Function2[-T1, -T2, +R] extends ...

What are the consequences of using covariance and contravariance?

For instance for the function:

val f: Function2[Animal, Cat, Dog] = ...

Which of these casts are illegal?

val f1: Function2[Animal, Cat, Animal] = f
val f2: Function2[Dog, Cat, Dog] = f
val f3: Function2[Cat, Cat, Dog] = f
val f4: Function2[Animal, Cat, Animal] = f
val f5: Function2[Animal, Cat, Cat] = f
val f6: Function2[Animal, Dog, Dog] = f

Summary

This is the result of applying variances:

References

See the official Scala documentation on variance.

Enjoy!