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 subtype relation Cat ⊑ Animal means that Cat is a subtype of
Animal. Informally, you’d say a Catis anAnimal—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.
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.
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 ⊑ B and G[A] ⊑ G[B],
then T in G[T] is covariant.
If A ⊑ B 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.
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.
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:
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.
classSetter[T]{defset(v:T):Unit={}}
Again, we can create one for Cats and one for Animals.
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.
The fusion of Object Oriented Programming (OOP) and Functional Programming (FP) languages has become increasingly popular in recent years, with many programming languages now offering features and constructs from both...