Menu

Covariance and Contravariance in Scala

July 06, 2015

The topic of covariance and contravariance is explained in many places (including Wikipedia). This post is my attempt to explain it in my own words, using Scala.

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 CatAnimal 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 AB, 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 CatAnimal. 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 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.
  • If neither is true, then T in G[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@10cf09e8

And 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.meow

f 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@6b063695

When 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.

categorySoftware Engineering