Variance of Parameterized Type

Scala prevented us from assigning a reference of ArrayList[Int] to a reference of ArrayList[Any] in Type Inference for Generics and Collections. That’s a good thing; in general a collection of derived should not be assignable to a collection of base. However, there are times when we want some lenience to that rule. In those situations, we can ask Scala to permit otherwise invalid conversions. Tighten your seat belts—we’re in for a fun, but intense, ride.

Covariance and Contravariance

Scala will stop at compile time any conversions that may potentially lead to runtime failures. Specifically, as an example, it prevents the following code from compiling:

 
var​ arr1 = ​new​ ​Array​[​Int​](3)
 
var​ arr2: ​Array​[​Any​] = null
 
 
arr2 = arr1 ​// Compilation ERROR

Here’s the error reported by the compiler:

 
NotAllowed.scala:4: error: type mismatch;
 
found : Array[Int]
 
required: Array[Any]
 
Note: Int <: Any, but class Array is invariant in type T.
 
You may wish to investigate a wildcard type such as `_ <: Any`. (SLS
 
3.2.10)
 
arr2 = arr1 // Compilation ERROR
 
^
 
one error found

The previous restriction is a good thing. Imagine if Scala—like Java—did not restrict that. Here is Java code that can get us into trouble:

MakingUseOfTypes/Trouble.java
Line 1 
//Java code
class​ Fruit {}
class​ Banana ​extends​ Fruit {}
class​ Apple ​extends​ Fruit {}
public​ ​class​ Trouble {
public​ ​static​ ​void​ main(​String​​[]​ args) {
Banana​[]​ basketOfBanana = ​new​ Banana[2];
basketOfBanana[0] = ​new​ Banana();
10 
Fruit​[]​ basketOfFruits = basketOfBanana;
basketOfFruits[1] = ​new​ Apple();
for​(Banana banana : basketOfBanana) {
15 
System​.out.println(banana);
}
}
}

The previous code will compile with no errors. However, when we run it, it will give the following runtime error:

 
Exception in thread "main" java.lang.ArrayStoreException: Apple
 
at Trouble.main(Trouble.java:12)

The reason for the error is, at runtime, we’re trying to place an apple into a basket of bananas, under the pretext of using a basket of fruits. While the failure happened on line 12, the root cause is that the Java compiler did not stop us on line 11.

While the previous problem slipped through the Java compiler, to be fair, it doesn’t allow the following:

 
//Java code
 
ArrayList​<​Integer​> list = ​new​ ​ArrayList​<​Integer​>();
 
ArrayList​<​Object​> list2 = list; ​// Compilation error

Unfortunately, it’s easy to bypass this in Java like this:

 
ArrayList​ list3 = list;

The ability to send a collection of subclass instances where a collection of base class instances is expected is called covariance. And the ability to send a collection of superclass instances where a collection of subclass instances is expected is called contravariance. By default Scala does not allow either one of them.

Supporting Covariance

Although the default behavior of Scala is good in general, there are genuine cases where we’d want to cautiously treat a collection of a derived type—say a collection of Dogs—as a collection of its base type—say a collection of Pets. Consider the following example:

MakingUseOfTypes/PlayWithPets.scala
 
class​ Pet(​val​ name: ​String​) {
 
override​ ​def​ toString = name
 
}
 
 
class​ Dog(​override​ ​val​ name: ​String​) ​extends​ Pet(name)
 
 
def​ workWithPets(pets: ​Array​[Pet]) {}

We’ve defined two classes—a Dog that extends a class Pet. We have a method workWithPets that accepts an array of Pets but really does nothing. Now, let’s create an array of Dogs:

MakingUseOfTypes/PlayWithPets.scala
 
val​ dogs = ​Array​(​new​ Dog(​"Rover"​), ​new​ Dog(​"Comet"​))

If we send the dogs to the previous method, we will get a compilation error:

 
workWithPets(dogs) ​// Compilation ERROR

Scala will complain at the call to workWithPets—we can’t send an array of Dogs to a method that accepts an array of Pets. But, the method is benign. However, Scala doesn’t know that, and it’s trying to protect us. We have to tell Scala that it’s okay to let this happen. Here’s an example of how we can do that:

MakingUseOfTypes/PlayWithPets.scala
 
def​ playWithPets[T <: Pet](pets: ​Array​[T]) =
 
println(​"Playing with pets: "​ + pets.mkString(​", "​))

We’ve defined the method playWithPets with a special syntax. T <: Pet indicates that the class represented by T is derived from Pet. This syntax is used to define an upper bound—if you visualize the class hierarchy, Pet is the upper bound of type TT can be any type Pet or lower in the hierarchy. By specifying the upper bound, we’re telling Scala that the parameterized type T of the parameter array must be at least an array of Pet but can be an array of any class derived from Pet. So, now we are allowed to make the following call:

MakingUseOfTypes/PlayWithPets.scala
 
playWithPets(dogs)

Here’s the corresponding output:

 
Playing with pets: Rover, Comet

If you try to send an array of Objects or an array of objects of some type that does not derive from Pets, you’ll get a compilation error.

Supporting Contravariance

Now let’s say we want to copy pets from one collection to another. We can write a method named copy that accepts two parameters of type Array[Pet]. However, that will not help us send an array of Dogs. Furthermore, we should be able to copy from an array of Dogs into an array of Pets. In other words, it’s okay for the receiving array to be a collection of supertypes of the class of the source array in this scenario. What we need is a lower bound:

MakingUseOfTypes/PlayWithPets.scala
 
def​ copyPets[S, D >: S](fromPets: ​Array​[S], toPets: ​Array​[D]) = { ​//...
 
}
 
 
val​ pets = ​new​ ​Array​[Pet](10)
 
copyPets(dogs, pets)

We’ve constrained the destination array’s parameterized type (D) to be a supertype of the source array’s parameterized type (S). In other words, S (for a source type like Dog) sets the lower bounds for the type D (for a destination type like Dog or Pet)—it can be any type that is type S or its supertype.

Customizing Variance on a Collection

In the previous two examples, we controlled the parameters of methods in the method definition. You can also control this behavior if you’re the author of a collection—that is, if you assume that it’s okay for a collection of derived to be treated as a collection of base. You can do this by marking the parameterized type as +T instead of T, as in the following example:

MakingUseOfTypes/MyList.scala
 
class​ MyList[+T] ​//...
 
var​ list1 = ​new​ MyList[​Int​]
 
var​ list2 : MyList[​Any​] = null
 
list2 = list1 ​// OK

Here, +T tells Scala to allow covariance; in other words, during type checking, it asks Scala to accept a type or its base type. So, we’re able to assign a MyList[Int] to MyList[Any]. Remember, this was not possible for Array[Int]. However, this is possible for the functional list List implemented in the Scala library—we’ll discuss this in Chapter 8, Collections.

Similarly, you can ask Scala to support contravariance on your types using -T instead of T for parameterized types.

By default, the Scala compiler strictly enforces the variance. We can request lenience for covariance or contravariance. In any case, the Scala compiler will check for type soundness of variance.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.58.121.251