Covariance and Contravariance in Scala
July 06, 2015
The code is also on gist.
Class hierarchies: the ⊑-relation
Take a look at this simple class hierarchy:
The sub type relation Cat
⊑ Animal
says, that dass Cat
is a sub type of
Animal
. In colloqial speech you say that a Cat
is an Animal
. Therefore
these hierarchies are also called is-a-hierarchies.
Do not make the mistake to translate the symbol ⊑ as "is-smaller-or-equal-than". It looks similiar
and a cat is a sub type, but it may have more information, methods and attributes.
An advantage of such a class hierarchy is, that you can write one function for all Animal
s.
You can mix Cat
s and Dogs
.
So in general at every expression that is of type Animal
you could substitute an expression
of type Cat
or Dog
.
In the theory of programming languages this property is called the
Liskov substitution principle.
If A
⊑ B
, then expressions of type B
can be replaced by expressions
of type A
.
Generic classes with type parameters
A Generic class has at least one type parameter T
.
This is useful for lists, sets, trees and other collections. You only have to define List[T]
once
and you can create lists of cats, dogs or animals in general.
Generic classes and the ⊑-relation: covariance and contravariance
In general it is not the case that a type parameter of a generic class respects the ⊑-relation.
We know, that Cat
⊑ Animal
. But how is it with
G[Cat]
and G[Animal]
? If Cat
is an
Animal
, is G[Cat]
also a G[Animal]
?
There are the following possibilities:
-
If
A
⊑B
andG[A]
⊑G[B]
, thenT
inG[T]
is covariant. -
If
A
⊑B
andG[B]
⊑G[A]
, thenT
inG[T]
is contravariant. -
Otherwise
T
inG[T]
is invariant.
There are only two directions for information: entering a class or leaving a class, into it or out of it, writing or reading, push or pop. Methods that read data from classes are called getters and method that write are called setters.
Getter
Take a look at this getter with type parameter T
:
You can create a Cat
-getter:
and later you can read the cat:
You also can create an Animal
-getter:
This is the same code, only that the type of the getter now is Getter[Animal]
that returns
an Animal
, even if a new Cat
is used in the constructor.
What is the relationship between Getter[Cat]
and Getter[Animal]
?
Can we replace all Getter[Cat]
with Getter[Animal]
or the other way around?
Let's call the getters and let's try to convert the results.
You see that the results of
gc.get
can be converted into Animal
, but
the result of
ga.get
can not be converted into a cat.
So therefore gc
is more general as ga
. We can't use ga
for
each gc
.
Conclusion: Getter[Cat]
⊑ Getter[Animal]
.
There is another way to see this. Extend the class Cat
with a method:
and now write the following function
f
does not accept Getter[Animal]
because an Animal
has no meow
.
But every Getter[Animal]
can be replaced by a Getter[Cat]
.
Therefore Getter[Cat]
⊑ Getter[Animal]
and T
in
Getter
is covariant.
Setter
A setter writes information into a class. The following class accepts an argument and forgets it immediately:
We create an instance for Cat
s and for Animal
s.
If we now try out the different combinations, the following happens:
Setter[Animal]
can take all animals as parameter, but the
Setter[Cat]
takes only cats. (Simple, isn't it).
So a Setter[Cat]
can be replaced by a Setter[Animal]
but not the other way round.
By the substitution principle: Setter[Animal]
⊑ Setter[Cat]
and T
in Setter[T]
is contravariant.
Conclusion
So the basics are explained.
If you want further information, i recommend the book by Odersky et. al.