Search code examples
scalagenericscovariancecontravariance

Implementing a method inside a Scala parameterized class with a covariant type


I've read a few tutorials including the main Scala documentation regarding method signatures of covariant types. Suppose I have the following abstract class:

abstract class List[+A] {

  def head: A
  def tail: List[A]
  def isEmpty: Boolean
  def add[B >: A](element: B): List[B]
  protected def printElements: String

  override def toString: String = "[" + printElements + "]"

}

My question concerns the signature of the add() method. Why is it necessary to declare it that way? We are passing in a parameter that is a supertype of A. What problem does this solve? I'm trying to understand this on an intuitive level.


Solution

  • Formal explanation

    Given

    abstract class List[+A] {
      def add(element: A): List[A]
    }
    

    "This program does not compile, because the parameter element in add is of type A, which we declared covariant. This doesn’t work because functions are contravariant in their parameter types and covariant in their result types. To fix this, we need to flip the variance of the type of the parameter element in add.
    We do this by introducing a new type parameter B that has A as a lower type bound".
    -- reference.

    Intuitive explanation

    In this example, if you add something to a List:
    It must be an A - in this case the List is still a List[A].
    Or it must be any subtype of A - in this case the element gets upcasted to A, and the List remains a List[A].
    Or if it is another type B, then it MUST be a supertype of A - in this case the List gets upcasted to a List[B]. (Note: Because Any is just a supertype of everything, in the worst case the List will be upcasted to List[Any]).