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