I have an intuition that I find helpful to understand covariance and contravariance. Note that this is not a strict definition though. The intuition is following:
- if some class only outputs values of the type
A
, which is the same as saying that the users of the class can only read values of the type A
from the class, it is covariant in the type A
- if some class only takes values of the type
A
as inputs, which is the same as saying that the users of the class can only write values of the type A
to the class, it is contravariant in the type A
For a simple example consider two interfaces Producer[A]
and Consumer[A]
:
trait Producer[A] {
def produce():A
}
trait Consumer[A] {
def consume(value:A):Unit
}
One just outputs values of type A
(so you "read" A
from Producer[A]
) while the other accepts them as a parameter (so you "write" A
to the Producer[A]
).
Now consider method connect
:
def connect[A](producer:Producer[A], consumer:Consumer[A]): Unit = {
val value = producer.produce()
consumer.consume(value)
}
If you think for a moment this connect
is not written in the most generic way. Consider types hierarchy Parent
<: Main
<: Child
.
- For a fixed
Consumer[Main]
it can process both Main
and Child
because any Child
is actually Main
. So Consumer[Main]
can be safely connected to both Producer[Main]
and Producer[Child]
.
- Now consider fixed
Producer[Main]
. It produces Main
. Which Consumer
s can handle that? Obviously Consumer[Main]
and Consumer[Base]
because every Main
is Base
. However Consumer[Child]
can't safely handle that because not every Main
is Child
So one solution to creating the most generic connect
would be to write it like this:
def connect[A <: B, B](producer:Producer[A], consumer:Consumer[B]): Unit = {
val value = producer.produce()
consumer.consume(value)
}
In other words, we explicitly say that there are two different generic types A
and B
and one is a parent of another.
Another solution would be to modify Producer
and Consumer
types in such a way that an argument of type Producer[A]
would accept any Producer
that is safe in this context and similarly that an argument of type Consumer[A]
would accept any Consumer
that is safe in this context. And as you may have already noticed the rules for Producer
are "covariant" but the rules for "Consumer" are "contravariant" (because remember you want Consumer[Base]
to be a safe subtype of Consmer[Main]
). So the alternative solution is to write:
trait Producer[+A] {
def produce():A
}
trait Consumer[-A] {
def consume(value:A):Unit
}
def connect[A](producer:Producer[A], consumer:Consumer[A]): Unit = {
val value = producer.produce()
consumer.consume(value)
}
This solution is better because it covers all cases by a single change. Obviously in any context that Consumer[Main]
is safe to be used Consumer[Base]
is also safe.