Variance (wikipedia, scala-lang) is a thing that seems trivial on the first glance but is a useful tool for guaranteeing type safety. It took me a while to grok all of its implications, and I’ll summarize variance and the “so what?” behind it.
In scala generic parameters of classes can be annotated with additional variance annotations. Those annotations impose further bounds on how the declared class can be used.
Variance is somewhat akin to the Liskov Substitution Principle. LSP states that subclasses should be used transparently in place of their superclasses.
Variances imposes some additional limitations on how we can correctly use a generic class in terms of sub- and super- typing.
An example of a covariant class in scala is the immutable Vector
.
Consider this class hierarchy which we will use for the examples:
Covariance defines the following relationship: if A
is a subtype of B
then Vector[A]
is a subtype of Vector[B]
. Concretely Vector[Dog]
is a
subtype of Vector[Animal]
.
Another way of looking at it is if a Vector is covariant we must be able to
use Vector[Dog]
in any place that we would use Vector[Animal]
because
Animal
is of type Dog
or wider. Covariance implies a conversion between a
narrower type and a wider type - you may treat Vector[Dog]
as if it’s
Vector[Animal]
.
If a class parameter is invariant it means that no conversion wider to narrower, nor narrower to wider may be performed on the class.
The most common usage of invariance are mutable collections. An Array
is an
example of an invariant class.
Invariance is important for type safety of mutable collections. Java, famously, has covariant mutable arrays. This should show you why it’s a bad idea:
Because of this users of arrays in can be fooled into thinking they are dealing
with Object[]
instead String[]
. If java would allow storing Integer
s into
a String
array this would blow up at the read site - the “readers” of the
array would think they are still dealing with a String
array but and not
a Object
array and try to read strings from it.
Contravariance is in some way the polar opposite of covariance. The canonical
example of a contrvariant class in scala is Function1[-T1, +R]
. Why does
Function1
need to be contrvariant on its the input parameter?
Let’s think in term of conversions. Covariance, which was discused before,
implies that there exists a conversion between Vector[Dog]
to Vector[Animal]
because Dog
is a subclass of Animal
. Does a similar conversion make sense in
the case of Function1
?
If we have Function1[Dog, Any]
then in the general case this function should
work for Dog
s and its subtypes. But not necessarily for animals because it may
use “features” (methods) that are only available to the subtype.
The reverse conversion works however - if we have a function that works on
Animals
then this function by design should work on Dogs
.
Contravariance means that if B
is a supertype of A
then Function1[A, R]
is a
supertype of Function1[B, R]
. Concretely Function1[Dog, Any]
is a
supertype of Function1[Animal, Any]
.
When defining a generic class with a var
field we can get compile time errors:
Let’s break it down a little. Why doesn’t the compiler allow getters in
the Covariant
class?
Why? Let’s think about usages of covariance let’s say that we have a class:
If the print
method can print Dog
s does it make sense (in general) that it
should also print Animals
? Maybe sometimes but in the general sense if we want
to generalize the Printer
class we should use contravariance. The compiler is
smart enough to check this type of usage for us.
Let’s think about the second use case: returning a generic parameter:
And again - does it make sense that Create
should generalize by
contravariance? If Create
returns instances of the Animal
class should we be
able to use it in every place that expects Create[Dog]
? The scala compiler is
smart enough that it explodes in our face if we try it.