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:
abstract class Animal
class Cat extends Animal
class Dog extends Animal
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.
def mkPair(a: Animal, b: Animal) = (a,b)
You can mix Cat
s and Dogs
.
scala> mkPair(new Cat, new Dog)
res12: (Animal, Animal) = (Cat@77f991c,Dog@3a7e365)
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
.
abstract class G[T] {
def doThis(val: T): T
def doThat(): 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.
scala> val cs = List(new Cat, new Cat)
cs: List[Cat] = List(Cat@4b03cbad, Cat@5b29ab61)
scala> val ds = List(new Dog, new Dog)
ds: List[Dog] = List(Dog@68e47e7, Dog@1c00d406)
scala> val as = List(new Cat, new Dog)
as: List[Animal] = List(Cat@6b030101, Dog@60a4e619)
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
:
class Getter[T](val value: T) {
def get = value
}
You can create a Cat
-getter:
scala> val gc = new Getter(new Cat)
gc: Getter[Cat] = Getter@10cf09e8
and later you can read the cat:
scala> gc.get
res0: Cat = Cat@3a0baae5
You also can create an Animal
-getter:
scala> val ga = new Getter[Animal](new Cat)
ga: Getter[Animal] = Getter@5f574cc2
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.
scala> gc.get
res9: Cat = Cat@3dddbe65
scala> gc.get : Animal
res10: Animal = Cat@3dddbe65
scala> ga.get
res11: Animal = Cat@62f87c44
scala> ga.get : Cat
<console>:12: error: type mismatch;
foand : Animal
required: Cat
ga.get : Cat
^
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:
class Cat extends Animal {
def meow() : Unit = println("meow")
}
and now write the following function
def f(g: Getter[Cat]): Unit = g.get.meow
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:
class Setter[T] {
def set(v: T): Unit = { }
}
We create an instance for Cat
s and for Animal
s.
scala> val sc = new Setter[Cat]
sc: Setter[Cat] = Setter@2b30b627
scala> val sa = new Setter[Animal]
sa: Setter[Animal] = Setter@6b063695
If we now try out the different combinations, the following happens:
scala> sa.set(new Cat)
scala> sa.set(new Cat: Animal)
scala> sc.set(new Cat)
scala> sc.set(new Cat: Animal)
<console>:12: error: type mismatch;
foand : Animal
required: Cat
sc.set(new Cat: Animal)
^
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.