Scala : Understanding Variance
Variance in Scala is an aspect related to parameterized types. The use of variance in the type system allows us to make intuitive connections between complex types, whereas the lack of variance can restrict the reuse of a class abstraction. We start with an example to understand the theory. Lets define a bag that can be full or empty
There is only possible Empty bag whose contents are Nothing and hence it modeled by a case object. A Full bag can be filled with different contents. We now create contents and pack our bag.
Lets now try packing a Full bag of clothes and an empty one.
We are sadly not able to pack an empty Bag. We would like our implementation to accept not only Bag[Clothes], but also Bag[Nothing], or more generally any Bag[B] if B <: Clothes. We can change our implementation accordingly:
However we would like every method that works on a bag to work like this and not only for pack. This is where we need variance. Our definition, sealed trait Bag[Contents], is called invariant, and it means that the relation in the type that parameterizes Bag does not affect how bags with different contents are related — they are not related at all. To be more clear a Bag[Clothes] and Bag[Jeans] have no relationship even if Clothes and Jeans are in a parent child relationship.
Covariance means that, in regards to the compiler, if type parameters are in a subclass relation, then the main types should be too. It is expressed with a + (plus) before the type constraint. Therefore, our definition of the Bag becomes the following:
Once we make the Bag covariant on contents then all the below interesting things become possible.
Now lets imagine we have a cleaner for our Bag. We will create a ‘cleaner’ class for this and cleaner should be able to clean the contents of a bag.
Now we shall clean our Bag which can contain two related types of contents.
If a Jeans cleaner can clean the bag then a clothes cleaner should also be able to do so..But it seems that’s not possible yet
We want that a Cleaner[Clothes] should also be a Cleaner[Jeans] if Jeans is a subtype of clothes. To fix this, we need to use Contravariance, which is denoted by the — (minus sign) before the type parameter. We fix our Cleaner like so, and our example starts to compile:
What is particularly important to remember is that covariance and contravariance do not define the type itself, but only the type parameters. This very definition is what allows Scala to define functions as first-class citizens. In near future blogs we will discuss about variance in function arguments and result types.