Types variance in Scala

on waitingforcode.com

Types variance in Scala

Lower and upper bounds are not a single Scala feature related to the types. Another one is the variance.

This post goes a little bit further with Scala's types and presents the variance. The first section explains the concept with a short introduction. The second section shows the link between variance and the Liskov substitution principle. The last part shows some test cases illustrating variance specificity.

Invariance, covariance and contrvariance

Variance is related to the ability to use more generic or more specific type than the type originally defined. Concretely it means that we can use derived types in container objects, functions and so on. For instance the following code won't compile because of the Error:(126, 63) type mismatch;found : InvariantContainer[Integer]required: InvariantContainer[Number] Note: Integer <: Number, but class InvariantContainer is invariant in type T. You may wish to define T as +T instead. (SLS 4.5):

class InvariantContainer[T] {}
val invariantIntContainer: InvariantContainer[Number] = new InvariantContainer[Integer]()

The compiler has just informed us about invariance. As the name indicates, invariance occurs when the compiler expects exactly the same type as defined in the code. So in the case presented above it expected to see an InvariantContainer[Number] but we gave it an InvariantContainer[Integer]. It's the default compilation strategy regarding the types. Invariance helps in such case to achieve type safety and the runtime Java errors as the one described below don't occur:

Object[] letters = new String[]{"a", "b", "c"};
letters[0] = 1;
// Fails with a runtime error:
// java.lang.ArrayStoreException: java.lang.Integer

The reason of this error is covariance. By default arrays in Java are covariant, i.e. they can accept more specific types than defined. Purely speaking it means that if we take an example of lists List[A] and List[B] where A is a subtype of B, then List[A] is a subtype of List[B]. Concretely it would mean that for 2 classes Parent and Child where Child extends Parent, the List[Child] is a subtype of List[Parent]. As you see, the variance helps here to establish subtypying relationships using generics. Covariance is expressed with [+A] type declaration.

For the situations where we need the opposite, i.e. more generic types, we can use contravariance. It's particularly useful for function parameters since the body can use some of features of declared type and using not adapted one would result in runtime errors. In Scala contravariance is expressed with [-A] sign. Purely speaking it means for a some parametrized classes ParamClass[A] and ParamClass[B] where A is a subtype of B that ParamClass[B] is a subtype of ParamClass[A].

Nothing type

A good example showing covariance is Option[+A] its None type implemented as:

  case object None extends Option[Nothing]
  

Since Nothing is a subtype of all other types and the Option is defined as covariant, the None case object applies to the option of any type.

The covariance and contravariance resumes the following image:

Liskov substitution principle and variance

Variance is related to the Liskov substitution principle (LSP) telling that "functions that use pointers to base classes must be able to use objects of derived classes without knowing it". In more human friendly language it means that if S is a subtype of T, then the objects of type T may be replaced with objects of type S without impacting any of its important T properties (= the derived types extend their supertypes without changing their behavior).

The classical example breaking the LSP is the one with square and rectangle (aka circle-ellipse problem):

class Rectangle {
  var width = 0
  var height = 0

  def changeWidth(newWidth: Int) = width = newWidth
  def changeHeight(newHeight: Int) = height = newHeight
}

class Square extends Rectangle {
  override def changeWidth(newWidth: Int): Unit = {
    width = newWidth
    height = newWidth
  }

  override def changeHeight(newHeight: Int): Unit = changeWidth(newHeight)
}

With so defined code it's completely legal to do:

def increaseRectangleAreaWithNewWidth(rectangle: Rectangle, newWidth: Int): Int = {
  rectangle.changeWidth(newWidth)
  rectangle.width * rectangle.height
}
val rectangle2x5 = new Rectangle()
rectangle2x5.changeWidth(2)
rectangle2x5.changeHeight(5)
val rectangle2x5ResizedArea = increaseRectangleAreaWithNewWidth(rectangle2x5, 3)
rectangle2x5ResizedArea shouldEqual 15

val square = new Square()
square.changeWidth(2)
square.changeHeight(5)
val squareResizedArea = increaseRectangleAreaWithNewWidth(square, 3)
squareResizedArea shouldEqual 9

But as you can correctly deduce, the area of rectangle doesn't change in the same way. For the case of rectangle2x5, it increased while for square it decreased. The square is unable to satisfy the requirement of Rectangle and its own specificity. If the LSP would be respected, then the Square class wouldn't alter the behavior of Rectangle. It proves then that either Square can't derive from Rectangle or the increaseRectangleAreaWithNewWidth parameter should be contravariant.

And it's here where the link between LSP and variance can be found in the LSP principle's requirements regarding to class signatures:

  • method arguments should be contrvariant
  • returned objects should be covariant

In Scala's library the respect of above requirements is implemented in the Function1 class, defined as:

trait Function1[@specialized(scala.Int, scala.Long, scala.Float, scala.Double) -T1,
                @specialized(scala.Unit, scala.Boolean, scala.Int, scala.Float, scala.Long, scala.Double) +R]

Variance examples

All 3 types of Scala's variance are illustrates in the following tests:

describe("variance types") {
  it("should apply contravariance to a method parameter") {
    trait Person

    class Pole extends Person {
      def sayCzesc: String = "Czesc"
    }
    class Englishman extends Person {
      def sayHello: String = "Hello"
    }
    class EnglishmanFromLiverpool extends Englishman {
      override def sayHello: String = "Hello from Liverpool"
    }
    abstract class ContravariantHouse[-T] {
      val address: String
    }
    class EnglishContravariantHouse extends ContravariantHouse[Englishman] {
      override val address: String = "England"
    }
    class LiverpoolContravariantHouse extends ContravariantHouse[EnglishmanFromLiverpool] {
      override val address: String = "Liverpool"
    }
    class PolishContravariantHouse extends ContravariantHouse[Pole] {
      override val address: String = "Poland"
    }
    def liveInContravariantEngland(house: ContravariantHouse[Englishman]): String = {
      s"Address: ${house.address}"
    }

    val addressCustom = liveInContravariantEngland(new ContravariantHouse[Person]() {
      override val address: String = "custom address"
    })
    val addressEngland = liveInContravariantEngland(new EnglishContravariantHouse())
    // The line below won't compile because of contravariance
    // liveInContravariantEngland(new LiverpoolContravariantHouse())

    addressCustom shouldEqual "custom address"
    addressEngland shouldEqual "England"
  }
  it("should apply covariance to the method parameter") {
    trait Person
    class Pole extends Person {
      def sayCzesc: String = "Czesc"
    }
    class Englishman extends Person {
      def sayHello: String = "Hello"
    }
    class EnglishmanFromLiverpool extends Englishman {
      override def sayHello: String = "Hello from Liverpool"
    }
    abstract class House[+T] {
      val person: T
    }
    class EnglishHouse(override val person: Englishman) extends House[Englishman]
    class LiverpoolHouse(override val person: EnglishmanFromLiverpool) extends House[EnglishmanFromLiverpool]
    class PolishHouse(override val person: Pole) extends House[Pole]

    def sayHelloFromHouseInhabitant(house: House[Englishman]): String = {
      house.person.sayHello
    }

    val englishHello = sayHelloFromHouseInhabitant(new EnglishHouse(new Englishman()))
    val liverpoolHello = sayHelloFromHouseInhabitant(new LiverpoolHouse(new EnglishmanFromLiverpool()))
    // As you can correctly deduce,
    // neither sayHelloFromHouseInhabitant(new PolishHouse(new Pole()))
    // nor any other house those type doesn't inherit directly from House[Englishman] won't work
    englishHello shouldEqual "Hello"
    liverpoolHello shouldEqual "Hello from Liverpool"
  }
  it("should apply covariance to a list") {
    trait Person
    class Pole extends Person {
      def sayCzesc: String = "Czesc"
    }
    class Englishman extends Person {
      def sayHello: String = "Hello"
    }
    class EnglishmanFromLiverpool extends Englishman {
      override def sayHello: String = "Hello from Liverpool"
    }
    val englishmen: List[Englishman] = List(new Englishman(), new EnglishmanFromLiverpool())

    val hellosFromEngland = englishmen.map(englishman => englishman.sayHello).mkString(", ")

    hellosFromEngland shouldEqual ("Hello, Hello from Liverpool")
  }
  it("should apply invariance") {
    trait Person
    class Englishman extends Person {
      def sayHello: String = "Hello"
    }
    class EnglishmanFromLiverpool extends Englishman {
      override def sayHello: String = "Hello from Liverpool"
    }
    abstract class Speaker[T](speaker: T) {
      def sayWelcome: String
    }
    class EnglishSpeaker[T]{}

    val englishSpeaker: EnglishSpeaker[Englishman] = new EnglishSpeaker[Englishman]()
    // As shown in the post, the expression below is invalid and the error about type mismatch is returned:
    // Error:(96, 69) type mismatch;
    // found   : EnglishSpeaker[EnglishmanFromLiverpool]
    // required: EnglishSpeaker[Englishman]
    // Note: EnglishmanFromLiverpool <: Englishman, but class EnglishSpeaker is invariant in type T.
    // You may wish to define T as +T instead. (SLS 4.5)
    // val englishSpeakerFromLiverpool: EnglishSpeaker[Englishman] = new EnglishSpeaker[EnglishmanFromLiverpool]()
  }
}

This post introduced the idea of variance in Scala. The first section described 3 available types: invariance, covariance and contravariance. As we could see, the variance improves type afety and the flexibility since it allows to safely pass more generic or more specific type into a method. It also establihes the inheritance relationship between parametrized types and clarifies the expected behavior. The second section shown the link between the Liskov substitution principle and the variance. As proven, the LSP requires the method parameters to be contravariance and the returned objects covariant. The last part showed some learning tests with the Scala's variances.

Share, like or comment this post on Twitter: