Scala Type Variance

09 Feb 2019 - Andrey Ilinykh

Type variance is an important part of scala type system. If you use scala for application programming, you probably don’t pay much attention to it. But as soon you start to create a library or just a piece of shared code, type variance becomes crucial.

So, let’s see in more details what type variance is and how to use it. Wikipedia tells us “variance refers to how subtyping between more complex types relates to subtyping between their components”. To get some intuition, we will create a simple toy - several traits and case classes:

scala> sealed trait Shape
defined trait Shape

scala> case class Circle(r: Double) extends Shape
defined class Circle

scala> case class Rectangle(w: Double, h: Double) extends Shape
defined class Rectangle

We have Shape trait here and two implementations- Circle and Rectangle. Both Circle and Rectangle are subtypes of Shape. It means a very simple thing. Everywhere we need Shape we can use Circle or Rectangle. It is called Liskov criteria.

scala> val s: Shape = Circle(10) //it is fine
s: Shape = Circle(10.0)

scala> val r:Rectangle = s //doesn't compile
<console>:15: error: type mismatch;
 found   : Shape
 required: Rectangle
       val r:Rectangle = s //doesn't compile
                         ^

Which makes perfect sense. Now we create one more type, CSV serializer.

scala> trait ShapeWriter[T <: Shape] {
     |   def write(s: T): String
     | }
defined trait ShapeWriter

scala> val shapeWriter = new ShapeWriter[Shape] {
     |   override def write(s: Shape) = {
     |     s match {
     |       case Circle(r) => s"Circle,${r.toString}"
     |       case Rectangle(w,h) => s"Rectangle,${w.toString},${h.toString}"
     |     }
     |   }
     | }
shapeWriter: ShapeWriter[Shape] = $anon$1@3b603c85

scala> shapeWriter.write(Circle(20))
res0: String = Circle,20.0

scala> shapeWriter.write(Rectangle(20, 10))
res1: String = Rectangle,20.0,10.0

As you can see our shapeWriter successfully writes both circles and rectangles. So far, so good. Now imagine, someone uses our code and he wants to write down rectangles only. We provide CSV writer for both circles and rectangles, so it is natural to reuse it.

scala> val rectangeWriter: ShapeWriter[Rectangle] = shapeWriter
<console>:16: error: type mismatch;
 found   : ShapeWriter[Shape]
 required: ShapeWriter[Rectangle]
Note: Shape >: Rectangle, but trait ShapeWriter is invariant in type T.
You may wish to define T as -T instead. (SLS 4.5)
       val rectangeWriter: ShapeWriter[Rectangle] = shapeWriter
                                                    ^

Oops, it doesn’t work. Why? Our writer is capable of writing rectangles. The problem is that the scala compiler doesn’t know it. For compiler ShapeWriter[Shape] and ShapeWriter[Rectangle] are just two different types. The error message tells us “Note: Shape >: Rectangle, but trait ShapeWriter is invariant in type T.” So, now we know what “invariant in type” means- there is no any relationship between types which are built from some type (Shape) and its subtype (Rectangle). How to fix it? Very easy. Scala compiler gives us a good advise. We have to mark type parameter by - symbol. The right definition of ShapeWriter is

trait ShapeWriter[-T <: Shape] {
  def write(s: T): String
}

Now we can use our shapeWriter as ShapeWriter[Rectangle]

val rectWriter: ShapeWriter[Rectangle] = shapeWriter
rectWriter.write(Rectangle(10, 10))

It means a ShapeWriter[Shape] is a subtype of ShapeWriter[Rectangle]. Such inversed relationship between simple types (Shape and Rectangle) and composite types (ShapeWriter[Shape] and ShapeWriter[Rectangle]) is called contravariance.

All functions In scala are contravariant in parameters. This is simplified defenition of Function1 trait:

trait Function1[ -T1,  +R] extends AnyRef {
  def apply(v1: T1): R
}

Let’s move forward and implement CSV reader. A type wich reads comma separated string and constructs a Shape. Naive defenition would be:

scala> trait ShapeReader[T <: Shape] {
     |   def read(str: String): T
     | }
defined trait ShapeReader

scala> val rectReader = new ShapeReader[Rectangle] {
     |   override def read(str: String) = {
     |     val ss = str.split(",")
     |     Rectangle(ss(1).toDouble, ss(2).toDouble)
     |   }
     | }
rectReader: ShapeReader[Rectangle] = $anon$1@3aa836ed

scala> rectReader.read(shapeWriter.write(Rectangle(10, 20)))
res2: Rectangle = Rectangle(10.0,20.0)

It reads rectangle successfully. But when we try to use it as a ShapeReader[Shape] we get a problem

scala> val shapeReader: ShapeReader[Shape] = rectReader // it doesn't compile
<console>:15: error: type mismatch;
 found   : ShapeReader[Rectangle]
 required: ShapeReader[Shape]
Note: Rectangle <: Shape, but trait ShapeReader is invariant in type T.
You may wish to define T as +T instead. (SLS 4.5)
       val shapeReader: ShapeReader[Shape] = rectReader // it doesn't compile
                                             ^

Again, we have the same problem- because ShapeReader[T <: Shape] is invariant in type T, ShapeReader[Shape] and ShapeReader[Rectangle] are different types. We can’t assign object of different types. The solution is simple - we just make our ShapeReader covariant:

scala> trait ShapeReader[+T <: Shape] {
     |   def read(str: String): T
     | }
defined trait ShapeReader

then we ca successfully run val shapeReader: ShapeReader[Shape] = rectReader. In other words ShapeReader[Rectangle] is a subtype of ShapeReader[Shape].

Following table summarizes the result. It describes relationship between types SomeType[T] and SomeType[S] when S is subtype of T

Variance Syntax Description
Covariant SomeType[+T] SomeType[S] is subtype of SomeType[T]
Contravariant SomeType[-T] SomeType[T] is subtype of SomeType[S]
Invariant SomeType[T] there is no any relationship

You may ask a question: Why do we need such overcomplicated things. Why not use the same variance everywhere? It turns out, the same variance makes the code less safe or makes impossible creation some very usefull classes. Let’s take some examples. Arrays in scala are invariant. The definition looks like final class Array[T].
Following code doesn’t compile in scala

scala> val rectArray = Array(Rectangle(10, 20))
rectArray: Array[Rectangle] = Array(Rectangle(10.0,20.0))

scala> val shapeArray: Array[Shape] = rectArray
<console>:14: error: type mismatch;
 found   : Array[Rectangle]
 required: Array[Shape]
Note: Rectangle <: Shape, but class Array is invariant in type T.
You may wish to investigate a wildcard type such as `_ <: Shape`. (SLS 3.2.10)
       val shapeArray: Array[Shape] = rectArray
                                      ^

But it works in Java because Java arrays are covariant. Imagine Arrays are covariant in scala as well. Then very easy we can get a problem. As shapeArray is array of shapes, we can put Circle there:

shapeArray(0) = Circle(5)

but because both shapeArray and rectArray point to the same object now, next line will crash our application

val area = rectArray(0).w * rectArray(0).h // runtime exception here

This code is correct in Java but not in scala.

Another possible way is to have everything invariant. But it brings problems as well. Just one example.

Scala class Option is covariant, it is defined as final class Option[+T]. Then we have case class Some[T](value: T) extends Option[T] and object None extends Option[Nothing] because Nothing is subtype of every other type we can assign it to any Option

scala> val r:Option[Circle]= None
r: Option[Circle] = None

But if we have all types invariant then Option[Circle] and Option[Nothing] are just different types and assignment is not possible.