Covariance and Contravariance in Scala
July 06, 2015
The code is also available as a Gist.
Class hierarchies: the ⊑-relation
Consider the following class hierarchy:
abstract class Animal
class Cat extends Animal
class Dog extends Animal
The subtype relation Cat ⊑ Animal means that Cat is a subtype of
Animal. Informally, you’d say a Cat is an Animal—an is-a hierarchy.
Even though ⊑ looks a bit like “less than,” you should not read it that way: a subtype can have more
information and behavior than its supertype.
One advantage of such a hierarchy is that you can write functions that operate on all Animals.
def mkPair(a: Animal, b: Animal) = (a,b)
Here, you can mix Cats and Dogs.
scala> mkPair(new Cat, new Dog)
res12: (Animal, Animal) = (Cat@77f991c,Dog@3a7e365)
More generally: anywhere an Animal is expected, you can pass a subtype such as Cat or Dog.
In programming language theory, this is known as the
Liskov substitution principle.
If A ⊑ B, then expressions of type B can be replaced (substituted) with
expressions of type A.
Generic classes with type parameters
A generic class has at least one type parameter, e.g. T.
abstract class G[T] {
def doThis(val: T): T
def doThat(): T
}
This is useful for lists, sets, trees, and other collections. You implement List[T] once and can then create
lists of cats, dogs, or mixed animals.
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
With type parameters on generic classes, the question is how they relate to the ⊑ relation.
We know that Cat ⊑ Animal. But what about G[Cat] and G[Animal]?
Put informally: if a Cat is an Animal, is a G[Cat] also a G[Animal]?
There are three possibilities:
-
If
A⊑BandG[A]⊑G[B], thenTinG[T]is covariant. -
If
A⊑BandG[B]⊑G[A], thenTinG[T]is contravariant. -
If neither is true, then
TinG[T]is invariant.
In the following sections, we’ll use deliberately simple examples. For a class, information flows in only two directions: in or out—i.e. write or read. Methods that read values are typically called getters, and methods that write values are called setters.
Getter
Consider a minimal getter with type parameter T:
class Getter[T](val value: T) {
def get = value
}For example, we can create a getter for a cat:
scala> val gc = new Getter(new Cat)
gc: Getter[Cat] = Getter@10cf09e8And later we can retrieve the cat:
scala> gc.get
res0: Cat = Cat@3a0baae5
We can also create an Animal getter:
scala> val ga = new Getter[Animal](new Cat)
ga: Getter[Animal] = Getter@5f574cc2
This is the same code, but now the getter is a Getter[Animal] and therefore returns an Animal,
even though we passed a new Cat into the constructor.
So what is the relationship between Getter[Cat] and Getter[Animal]?
Can we replace every occurrence of Getter[Cat] with a Getter[Animal], or vice versa?
Let’s call the getters and 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;
found : Animal
required: Cat
ga.get : Cat
^
The result of gc.get can be converted to an Animal, while the result of
ga.get cannot be converted to a Cat.
So gc is more specific than ga. We cannot use ga everywhere a gc is expected,
and we cannot substitute gc with ga.
Therefore, Getter[Cat] ⊑ Getter[Animal].
We can also show this another way. Extend Cat with a method:
class Cat extends Animal {
def meow() : Unit = println("meow")
}and write the following function:
def f(g: Getter[Cat]): Unit = g.get.meowf does not accept a Getter[Animal] because it’s not guaranteed that the returned animal
has a meow method. Conversely, any occurrence of Getter[Animal] can be replaced with
a Getter[Cat].
So we have Getter[Cat] ⊑ Getter[Animal], and T in Getter is
covariant.
Setter
A setter only writes. Consider the following class: it accepts an argument and immediately forgets it.
class Setter[T] {
def set(v: T): Unit = { }
}
Again, we can create one for Cats and one for Animals.
scala> val sc = new Setter[Cat]
sc: Setter[Cat] = Setter@2b30b627
scala> val sa = new Setter[Animal]
sa: Setter[Animal] = Setter@6b063695When trying different combinations, we get the following:
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;
found : Animal
required: Cat
sc.set(new Cat: Animal)
^
Setter[Animal] can accept any animal, while Setter[Cat] accepts only cats—which is exactly what you’d expect.
So we can replace Setter[Cat] with Setter[Animal].
By the substitution principle, this means: Setter[Animal] ⊑ Setter[Cat],
and T in Setter[T] is contravariant.
Conclusion
That covers the core intuition behind both terms. If you want to go deeper, I recommend the book by Odersky et al.