I am trying to understand variance and in the book that I bought it explains as follow:
• A type with an unannotated parameter Foo[A] is invariant in A. This means there is no relationship between Foo[B] and Foo[C] no matter what the sub- or super-type relationship is between B and C.
• A type with a parameter Foo[+A] is covariant in A. If C is a subtype of B, Foo[C] is a subtype of Foo[B].
• A type with a parameter Foo[-A] is contravariant in A. If C is a supertype of B, Foo[C] is a subtype of Foo[B].
I have trouble to understand this sentence:
If C is a supertype of B, Foo[C] is a subtype of Foo[B].
Why not:
Foo[C] is a supertype of Foo[B].
C
is a supertype B
but why C
change suddenly to subtype of B
in contravariant?
C
is a supertypeB
but whyC
change suddenly to subtype ofB
in contravariant?
That is the definition of contravariance, it reverses the relation order (in our case, the "is subtype of" relation <:
).
Notice that it is not that C
is now a subtype of B
, that relationship is fixed, it is the thing that is a container of C
, i.e. Foo[C]
, is now a subtype of the container of B
, Foo[B]
, not directly B
itself.
The classic example for contravariance is function objects. Functions in Scala are contravariant in their argument type and covariant in their return type, i.e. Function1[-T, +R]
.
Lets see an example. Assume we have a small ADT:
sealed trait Animal
case class Mouse() extends Animal
case class Lion() extends Animal
And now we want to create a function from Lion => String
. Can we feed it a concrete function from Animal => String
?
def main(args: Array[String]): Unit = {
val animalToString: (Animal) => String = an => an.toString
val lionToString: (Lion) => String = animalToString
lionToString(new Lion())
}
Why does this compile? Because when you invoke lionToString
with a Lion
, you know for sure that it'll be able to invoke any function defined on Animal
, because Lion <: Animal
. But the other way around isn't true. Assume Function1
was covariant in its argument type:
def main(args: Array[String]): Unit = {
val lionToString: (Lion) => String = an => an.toString
val animalToString: (Animal) => String = lionToString
lionToString(new Mouse()) // <-- This would blow up.
}
Then we'd be able to pass a different subtype of Animal
when our function actually expects a Lion
.