This is an excerpt from the 2nd Edition of the Scala Cookbook (partially modified for the internet). This is Recipe 19.5, “How (and why) to make immutable collections covariant.”
Scala Problem
You want to create a Scala class whose generic parameters can’t be changed (they’re immutable), and want to understand how to specify it.
Solution
When you’re defining a Scala class and want to declare that generic type parameter elements can’t be changed, declare them to be covariant by defining them with a leading +
symbol, such as +A
. As an example of this, notice that the immutable collection classes like List
, Vector
, and Seq
are all defined to use covariant generic type parameters:
class List[+T]
class Vector[+A]
trait Seq[+A]
By making the type parameter covariant, the generic parameter can’t be mutated, but the benefit is that the class can later be used in a more flexible manner.
To demonstrate the usefulness of this, modify the example from the previous recipe slightly. First, define the class hierarchy:
trait Animal:
def speak(): Unit
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]): Unit = dogs.foreach(_.speak())
As with the ArrayBuffer
in the previous recipe, you can pass a Seq[Dog]
into makeDogsSpeak
without a problem:
// this works
val dogs = Seq(Dog("Nansen"), Dog("Joshu"))
makeDogsSpeak(dogs)
However, in this case, you can also pass a Seq[SuperDog]
into the makeDogsSpeak
method successfully:
// this works too
val superDogs = Seq(
SuperDog("Wonder Dog"),
SuperDog("Scooby")
)
makeDogsSpeak(superDogs)
Because Seq
is immutable and defined with a covariant generic type parameter as Seq[+A]
, makeDogsSpeak
can accept both Seq[Dog]
and Seq[SuperDog]
, without the conflict that was built up in the previous lesson.
Discussion
You can further demonstrate this by creating your own custom class with a covariant generic type parameter. To do this — and to keep things simple — create a collection class that can hold one element. Because you don’t want the collection element to be mutated, define the parameter as a val
, and make it covariant with +A
:
class Container[+A] (val elem: A)
----
Using the same type hierarchy that’s shown in the Solution, modify the makeDogsSpeak
method to accept a Container[Dog]
:
def makeDogsSpeak(dogHouse: Container[Dog]): Unit = dogHouse.elem.speak()
With this setup, you can pass a Container[Dog]
into makeDogsSpeak
:
val dogHouse = Container(Dog("Xena"))
makeDogsSpeak(dogHouse)
Finally, because you declared the element to be covariant with the +
symbol, you can also pass a Container[SuperDog]
into makeDogsSpeak
:
val superDogHouse = Container(SuperDog("Wonder Dog"))
makeDogsSpeak(superDogHouse)
Because the Container
element is immutable and its generic 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 discussed in the Variance lesson in the Scala Cookbook, and demonstrated in these examples, defining a container-type class with an immutable, generic type parameter makes the collection more flexible and useful throughout your code. As shown in this example, you can pass a Container[SuperDog]
into a method that expects to receive a Container[Dog]
.
Note that the Variance discussion also notes that the +A
symbol is your way of telling the compiler that the generic parameter A
will only be used as the return type of methods in this class, i.e., the “out” position. For instance, in this example, this code is valid:
class Container[+A] (val elem: A):
// 'A' is correctly used in the “out” position
def getElemAsTuple: (A) = (elem)
But any attempt to use an element of type A
as a method input parameter inside the class will fail with this error:
class Container[+A] (val elem: A):
def foo(a: A) = ???
^^^^
error: covariant type A occurs in contravariant position
in type A of parameter a
As that code shows, even though I don’t even try to implement the body of the foo
method, the compiler states that the A
type can’t be used in the “in” position.