Back2Basics: Demystifying Variance

In this blog, we will explore types of variances in Scala.

  • In-variance
  • Co-variance
  • Contra-variance

First, let’s talk about Types in Scala.

In the above example, ref1 is of type Any and ref2 is of type String. Scala Compiler has no problem if we assign String type to Any type. Let’s us now try it with a collection type.

Notice, we got an error when we assign ArrayList[String] to ArrayList[Any], saying ArrayList is invariant i.e there is type mismatch and we can’t do it. This is where Type Variance comes in. Type variance is where you can take a collection of one type and assign it to a collection of another type typically between subclasses relationships. Scala, unlike Java, protects us from accidentally assigning a collection of one type to a collection of another type even when types may be related. Let’s try again with the same example but using List instead of ArrayList.

Notice, when we tried assigning a list of String to list of Any, we didn’t get any error. This is because Scala List is different from Java’s ArrayList as Scala List is not invariant.

Next, let’s understand what covariance and contravariance are and how we can we can customize them when using our own custom collection types.

Covariance is defined as sending a collection of subclass instance where a collection of a base class instance is expected.
Let us understand this with the help of an example.

We have defined a method printAnimals which prints all the animals. We know that Dog is also an Animal. But when we pass a list of Dogs where a list of animals is expected, Scala compiler does not allow us to do so and throws type mismatch error. It does so because Scala protects us from accidentally assigning a collection of one type to a collection of another type. So, in order to be able to pass a list of dogs, we need to inform Scala compiler that is it okay if we pass a list of Dogs as Dog is a related subtype of Animal. We can do so with the help of covariant notation.

<: notation tells Scala compiler to accept any class whose upper bound is Animal. Now, let us pass ArrayList of Dogs to printAnimals method.

Notice, this time it works fine.

Let us now understand how to customize covariance with our own collection type.

A type parameter T of a generic class can be made covariant by using the annotation +T.

For some class Printer[+T], making T covariant which implies that for two types A and B where A is a supertype of B, class Printer[A] is a supertype of Printer[B]. This allows us to make very useful and intuitive subtyping relationships using generics.

We have defined a covariant class Printer.

Now, the printAnimals method takes Printer[Animal] as an input, so, if Printer knows how to print an animal then it should also know how to print a dog as a dog is itself an animal, making Printer class covariant class allows us to do so.

printAnimal method can now print an animal as well as a dog.

Contravariance is defined as sending a collection of a base class instance where a collection of a derived class instance is expected. Consider the following example, where we have created a copyFrom method,

We can pass ArrayList of Dog to copyFrom method.

But, if we try to pass an ArrayList of Animals where an ArrayList of Dog is expected, as we know that a Dog can also copy from Animal as Dog is also an Animal, the Scala compiler does not allow us to do so.

Contravariance enables us to do so. >: notation tells Scala compiler to accept class whose lower bound is Dog. In the below code snippet S and D are type parameters where S is super class and D is derived class. So, we are making S contravariant such that it can accept its superclass.

Let’s see how we can customize contravariance with our own collection types.

We have defined a contravariant class Printer.

A type parameter T of a generic class can be made contravariant by using the annotation -T. This creates a subtyping relationship between the class and its type parameter that is similar, but opposite to what we get with covariance. For some class Printer[-T], making T contravariant implies that for two types A and B where A is a subtype of B, class Printer[A] is a supertype of Printer[B]. This allows us to make very useful and intuitive subtyping relationships using generics.

Now, the printAnimals method takes Printer[Dog] as an input, so, if Printer knows how to print a dog then it can also know how to print an animal, making Printer class contravariant allows us to do so. Thus, we can send Printer[Animal] where Printer[Dog] is expected because with contravariant behavior Printer[Animal] is a subtype of Printer[Dog] as Animal is a supertype of Dog.

printAnimal method can now print an animal as well as a dog.

In this way, you can also use covariance and contravariance where it is needed.

I hope after reading this blog, you all would have understood variance well. Thanks for reading.
Happy blogging.

References:


knoldus-advt-sticker


Written by 

I am a Software Consultant and has experience of more than 1.5 years. I like to study and write about latest technologies.

3 thoughts on “Back2Basics: Demystifying Variance

Leave a Reply

%d bloggers like this: