CoVariant, ContraVariant and InVariant... Variances in Scala
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 ofList[Animal] - Reuse a function
Animal=>Booleanas a functionDog=>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:
getNamegetBreed- 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 <: AnimalimpliesClz[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 <: AnimalimpliesClz[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.