liveBook · Manning

Covariance

As explained in Functional Programming in Scala, Second Edition

Covariance is a type-system property in which subtyping of a type argument induces the same direction of subtyping for the parameterized type.

Covariance makes subtype relationships flow in the same direction through a parameterized type: if Dog is a subtype of Animal, List[Dog] is a subtype of List[Animal]. Scala marks a covariant parameter with +, as in enum List[+A]. section 3.1

Empty values and subtype flexibility

Nil extends List[Nothing]. Because Nothing is a subtype of every type and List is covariant, an empty list can be used as List[Int], List[Double], or another List type. For example: val ex1: List[Double] = List.Nil. section 3.1

Covariance supports several practical design choices:

  • It improves type inference for constructors such as Nil and for APIs built from covariant data types. section 15.2 section 3.1
  • Leaving out + makes the parameter invariant, so the subtype relationship no longer follows automatically. section 3.1
  • Pull[+O, +R] uses covariance in both output and result types, allowing Result[+R] to be Pull[Nothing, R] and Output[+O] to be Pull[O, Unit]. section 15.2
Listing 15.2 The Pull data type
enum Pull[+O, +R]:
  case Result[+R](result: R) extends Pull[Nothing, R]
  case Output[+O](value: O) extends Pull[O, Unit]
  case FlatMap[X, +O, +R](
    source: Pull[O, X],
    f: X => Pull[O, R]) extends Pull[O, R]
 
  def step: Either[R, (O, Pull[O, R])] = this match            #1
    case Result(r) => Left(r)
    case Output(o) => Right(o, Pull.done)
    case FlatMap(source, f) =>
      source match
        case FlatMap(s2, g) =>
          s2.flatMap(x => g(x).flatMap(y => f(y))).step
        case other => other.step match
          case Left(r) => f(r).step
          case Right((hd, tl)) => Right((hd, tl.flatMap(f)))
 
  @annotation.tailrec
  final def fold[A](init: A)(f: (A, O) => A): (R, A) =         #2
    step match
      case Left(r) => (r, init)
      case Right((hd, tl)) => tl.fold(f(init, hd))(f)
 
  def toList: List[O] =                                        #3
    fold(List.newBuilder[O])((bldr, o) => bldr += o)(1).result
 
  def flatMap[O2 >: O, R2](
    f: R => Pull[O2, R2]
  ): Pull[O2, R2] =                                            #4
    Pull.FlatMap(this, f)
 
  def >>[O2 >: O, R2](next: => Pull[O2, R2]): Pull[O2, R2] =
    flatMap(_ => next)
 
  def map[R2](f: R => R2]): Pull[O, R2] =
    flatMap(r => Result(f(r)))

Inspect the covariant Pull definition and its Result and Output constructors to see covariance applied to both type parameters. section 15.2

How Functional Programming in Scala, Second Edition covers it

How deep it goes
Defines covariance, contrasts invariance, and works through List, Nil, Nothing, and Pull without developing variance rules internally.
Examples
Uses List[Dog] versus List[Animal], Nil as multiple list types, covariant Pull, and Parser[+_] as concrete Scala examples.
What it assumes
Assumes generic types and subtyping, while introducing variance annotations through List and Pull examples.
What you can do after
Evaluate/understand when a Scala type parameter should be covariant and predict resulting subtype relationships.
How it connects
Connects covariance with subtyping, parameterized types, type inference, Nothing, algebraic data types, and stream processing.

Also explained in

Compare how 3 books explain covariance →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Functional Programming in Scala, Second Edition. The section links point to the parts of the book it draws on.