How (and why) to make immutable collections covariant

This is an excerpt from the Scala Cookbook (partially modified for the internet). This is Recipe 19.5, “How (and why) to make immutable collections covariant.”

Problem

You want to create a collection whose elements can’t be changed (they’re immutable), and want to understand how to specify it.

Solution

You can define a collection of immutable elements as invariant, but your collection will be much more flexible if you declare that your type parameter is covariant. To make a type parameter covariant, declare it with the + symbol, like [+A].

Covariant type parameters are shown in the Scaladoc for immutable collection classes like List, Vector, and Seq:

class List[+T]
class Vector[+A]
trait Seq[+A]

By defining the type parameter to be covariant, you create a situation where the collection can be used in a more flexible manner.

To demonstrate this, modify the example from the previous recipe slightly. First, define the class hierarchy:

trait Animal {
    def speak
}

class Dog(var name: String) extends Animal {
    def speak { println("Dog says woof") }
}

class SuperDog(name: String) extends Dog(name) {
    override def speak { println("I'm a SuperDog") }
}

Next, define a makeDogsSpeak method, but instead of accepting a mutable ArrayBuffer[Dog] as in the previous recipe, accept an immutable Seq[Dog]:

def makeDogsSpeak(dogs: Seq[Dog]) {
    dogs.foreach(_.speak)
}

As with the ArrayBuffer in the previous recipe, you can pass a sequence of type [Dog] into makeDogsSpeak without a problem:

// this works
val dogs = Seq(new Dog("Fido"), new Dog("Tanner"))
makeDogsSpeak(dogs)

However, in this case, you can also pass a Seq[SuperDog] into the makeDogsSpeak method successfully:

// this works too
val superDogs = Seq(new SuperDog("Wonder Dog"), new SuperDog("Scooby"))
makeDogsSpeak(superDogs)

Because Seq is immutable and defined with a covariant parameter type, makeDogsSpeak can now accept collections of both Dog and SuperDog.

Discussion

You can demonstrate this by creating a collection class with a covariant type parameter.

To do this, create a collection class that can hold one element. Because you don’t want the collection element to be mutated, define the element as a val, and make the type parameter covariant with +A:

class Container[+A] (val elem: A)

Using the same type hierarchy as shown in the Solution, modify the makeDogsSpeak method to accept a Container[Dog]:

def makeDogsSpeak(dogHouse: Container[Dog]) {
    dogHouse.elem.speak()
}

With this setup, you can pass a Container[Dog] into makeDogsSpeak:

val dogHouse = new Container(new Dog("Tanner"))
makeDogsSpeak(dogHouse)

Finally, to demonstrate the point of adding the + symbol to the parameter, you can also pass a Container[SuperDog] into makeDogsSpeak:

val superDogHouse = new Container(new SuperDog("Wonder Dog"))
makeDogsSpeak(superDogHouse)

Because the Container element is immutable and its mutable type parameter is marked as covariant, all of this code works successfully. Note that if you change the Container’s type parameter from +A to A, the last line of code won’t compile.

As demonstrated in these examples, defining an immutable collection to take a covariant generic type parameter makes the collection more flexible and useful throughout your code.