Covariance and Contravariance in Scala

July 06, 2015

I explain "covariance and contravariance" with some simple Scala examples.

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 CatAnimal 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 AB, 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 CatAnimal. 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 AB and G[A]G[B], then T in G[T] is covariant.
  • If AB and G[B]G[A], then T in G[T] is contravariant.
  • Otherwise T in G[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 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@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.