Julien Richard-Foy’s blog

Be friend with covariance and contravariance

It seems that there are not a lot of resources on Internet that explain what covariance and contravariance are. Let’s try to fix that in a blog post.

First, you need a meaningful domain model to make the code less abstract and easier to understand: a zoo with animals, like crocodiles, zebras and giraffes. You can model this using the following class diagram:

UML diagram

Covariance

In your zoo each animal lives in a fenced run. You model this with the following Run[A] type, that represents a run where an animal of type A lives in:

abstract class Run[A] {
  def get: A
}

The get method returns the animal that lives in the run.

Your zoo takes a great care of mammals well being: you want them to live in a run that is at least ten times larger than the mammal itself. For this you define a function isLargeEnough that takes a Run[Mammal] and tests if the run is large enough for the mammal that lives in (assuming that you have methods that give you the size of a mammal and the size of a run):

def isLargeEnough(run: Run[Mammal]): Boolean =

Now the question is: “can you pass a run of zebras to this function?”. After all, a Zebra is a Mammal, so if your function wants to get the underlying mammal of the run it will be happy to get a zebra, right? However, if you try the code you get the following compilation error:

scala> isLargeEnough(zebraRun)
<console>:14: error: type mismatch;
  found   : Run[Zebra]
  required: Run[Mammal]

It happens that Run[Zebra] is not a subtype of Run[Mammal], despite Zebra being a subtype of Mammal. More generally, you want Run[B] to be a subtype of Run[A] if B is a subtype of A. And that’s exactly what covariance is. You can tell the Scala compiler that Run[A] is covariant on A as follows:

abstract class Run[+A] {
  def get: A
}

Problem solved: the compiler now lets you pass a Run[Zebra] to isLargeEnough.

Contravariance

Your zoo employs several veterinaries that check animals health. Some of them are specialized for a given specy. You model them with the following Vet[A] type, that represents a veterinary that knows how to treat an animal of type A:

abstract class Vet[A] {
  def treat(a: A)
}

You need just one vet to treat all the mammals of your zoos. You define the following function that treats all mammals using a vet:

def treatMammals(vet: Vet[Mammal]) { … }

Now, the question is “can you pass a vet of animals to treatMammals?”. After all, a Mammal is an Animal, so if you have a vet that is able to treat animals he will be happy if you give it a mammal, right? However, if you try the code you get the following compilation error:

scala> treatMammals(animalVet)
<console>:14: error: type mismatch;
  found   : Vet[Animal]
  required: Vet[Mammal]

It happens that Vet[Animal] is not a subtype of Vet[Mammal], despite Mammal being a subtype of Animal. More generally, you want Vet[A] to be a subtype of Vet[B] if B is a subtype of A. And that’s exactly what contravariance is. You can tell the Scala compiler that Vet[A] is contravariant on A as follows:

abstract class Vet[-A] {
  def treat(a: A)
}

Problem solved: the compiler now lets you pass a Vet[Animal] to treatMammals.

Covariance, contravariance and invariance

A bunch of questions may have popped in your mind. What makes Run[A] covariant and Vet[A] contravariant? Why is that the Scala compiler does not help you with this? Why do we need this distinction?”

Covariance and contravariance are useful because they allow you to define more general types: types that accept more subtypes. It lets you use a Run[Zebra] in place of a Run[Mammal] and a Vet[Animal] in place of a Vet[Mammal]. By default, type parameters are invariants because the compiler has no way to guess what you intend to model with a given type, so it takes the strictest option (the one accepting the fewest subtypes) because it is always easy to relax this choice later.

On the other hand, the compiler “helps” you by forbidding the definition of types that may be unsound. For instance, if you try to make Vet covariant you’ll get the following compilation error:

scala> abstract class Vet[+A] { def treat(a: A) }
<console>:7: error: covariant type A occurs in contravariant position in type A of value a
       abstract class Vet[+A] { def treat(a: A) }
                                          ^

Indeed, by defining Vet to be covariant you would say that a Vet[Zebra] could be used in place of a Vet[Mammal], in other words you would say that a vet that only knows how to treat zebras could be used to treat any mammal!

If you look at the definitions of Run[+A] and Vet[-A] you may notice that the type A appears only in the return type of methods of Run[+A] and only in the parameters of methods of Vet[-A]. More generally a type that produces values of type A can be made covariant on A (as you did with Run[+A]), and a type that consumes values of type A can be made contravariant on A (as you did with Vet[-A]).

From the above paragraph you can deduce that types that only have getters can be covariant (in other words, immutable data types can be covariant, and that’s the case for most of the data types of Scala’s standard library), but mutable data types are necessarily invariant (they have getters and setters, so they both produce and consume values).

blog comments powered by Disqus