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.

scala> import java.util.ArrayList

scala> var ref1: Any = "Hi"
ref1: Any = Hi

scala> var ref2: String = "Hi"
ref2: String = Hi

scala> ref1 = ref2
ref1: Any = Hi 

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.

scala> var arrayList1: ArrayList[Any] = new ArrayList[Any]
arrayList1: java.util.ArrayList[Any] = []

scala> var arrayList2: ArrayList[String] = new ArrayList[String]
arrayList2: java.util.ArrayList[String] = []

scala> arrayList1 = arrayList2
:14: error: type mismatch;
 found   : java.util.ArrayList[String]
 required: java.util.ArrayList[Any]
Note: String <: Any, but Java-defined class ArrayList is invariant in type E.
You may wish to investigate a wildcard type such as `_ <: Any`. (SLS 3.2.10)        arrayList1 = arrayList2                     ^ 

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.

 scala> var listOfString = List("type", "variance")
listOfString: List[String] = List(type, variance)

scala> var listOfAny = List(1, "type")
listOfAny: List[Any] = List(1, type)

scala> listOfAny = listOfString
listOfAny: List[Any] = List(type, variance)

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.

scala> dogList.add(new Dog)
res5: Boolean = true

scala> printAnimals(dogList)
$line18.$read$$iw$$iw$Dog@1978eab7

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.

scala> class Printer[+T]
defined class Printer

We have defined a covariant class Printer.

scala> def printAnimals(animal: Printer[Animal]) = println("prints animal")
printAnimals: (animal: Printer[Animal])Unit

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.

scala> printAnimals(new Printer[Dog])
prints animal

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,

scala> def copyFrom(dog1: ArrayList[Dog], dog2: ArrayList[Dog]) = {
| println("Can copy!!!")
| }
copyFrom: (dog1: java.util.ArrayList[Dog], dog2: java.util.ArrayList[Dog])Unit

We can pass ArrayList of Dog to copyFrom method.

scala> val dogList1 = new ArrayList[Dog]
dogList: java.util.ArrayList[Dog] = []

scala> val dogList2 = new ArrayList[Dog]
dogList: java.util.ArrayList[Dog] = []

scala> copyFrom(dogList1, dogList2)
Can copy!!!

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.

scala> var animalList = new ArrayList[Animal]
animalList: java.util.ArrayList[Animal] = []

scala> copyFrom(dogList, animalList)
:16: error: type mismatch;
found : java.util.ArrayList[Animal]
required: java.util.ArrayList[Dog]
Note: Animal >: Dog, but Java-defined class ArrayList is invariant in type E.
You may wish to investigate a wildcard type such as `_ >: Dog`. (SLS 3.2.10)
copyFrom(dogList, animalList)
^

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.

scala> def copyFrom[D, S >: D](dog1: ArrayList[D], dog2: ArrayList[S]) = {
| println("Can copy!!!")
| }
copyFrom: [S, D >: S](dog1: java.util.ArrayList[S], dog2: java.util.ArrayList[D])Unit

scala> copyFrom(dogList, animalList)
Can copy!!!

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

scala> class Printer[-A]
defined class Printer

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.

scala> def printAnimals(animal: Printer[Dog]) = println("prints animal")
printAnimals: (animal: Printer[Animal])Unit

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.

scala> printAnimals(new Printer[Animal])
prints animal

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


2 comments

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s