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 Animals.
def mkPair(a: Animal, b: Animal) = (a,b)
You can mix Cats 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⊑BandG[A]⊑G[B], thenTinG[T]is covariant. -
If
A⊑BandG[B]⊑G[A], thenTinG[T]is contravariant. -
Otherwise
TinG[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@10cf09e8and 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 Cats and for Animals.
scala> val sc = new Setter[Cat]
sc: Setter[Cat] = Setter@2b30b627
scala> val sa = new Setter[Animal]
sa: Setter[Animal] = Setter@6b063695If 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.