पाठ 13 / 25

Generics, Variance and Bounds

Write generic code and understand covariance, contravariance and bounds.

Generic types done right

Classes, traits and methods take type parameters in square brackets: class Box[A](value: A), def firstOr[A](xs: List[A], default: A): A. Variance describes how subtyping of type arguments relates to subtyping of the generic type. Covariant +A means List[Dog] is a List[Animal], which is safe for immutable producers such as List, Option and Vector. Contravariant -A means a Printer[Animal] can be used where a Printer[Dog] is expected, suitable for consumers such as function parameters (Function1[-A, +B]). Invariant A (the default) allows neither, which is required for mutable containers such as Array and mutable.Buffer. Upper bounds A <: Shape restrict a type parameter to subtypes, and lower bounds B >: A allow methods like prepend[B >: A](b: B): List[B] to widen the element type safely. Higher-kinded types (F[_]) abstract over containers themselves and underpin libraries such as Cats. The compiler checks variance positions so you cannot accidentally make an unsafe type covariant.

Variance at a glance

Covariant types follow subtyping, contravariant types reverse it, invariant types ignore it.

Three pairs of boxes with arrows: one pair with arrows in the same direction, one reversed, and one with no arrow between them.
Figure 5.1 — Covariance, contravariance and invariance.

Variance and bounds

A covariant result type and a contravariant handler.

sealed trait Animal { def name: String }
case class Dog(name: String) extends Animal
case class Cat(name: String) extends Animal

sealed trait Result[+A]                              // covariant: Result[Dog] <: Result[Animal]
case class Ok[+A](value: A) extends Result[A]
case class Err(message: String) extends Result[Nothing]  // Nothing fits any Result[A]

trait Handler[-A]:                                    // contravariant: consumes A
  def handle(a: A): Unit

val animalLogger: Handler[Animal] = a => println(s"saw ${a.name}")
val dogHandler: Handler[Dog] = animalLogger            // OK: handles any animal, so handles dogs

val fetched: Result[Animal] = Ok(Dog("Bruno"))         // OK thanks to +A

def loudest[A <: Animal](animals: List[A]): Option[A] = // upper bound
  animals.maxByOption(_.name.length)

val pets: List[Animal] = Cat("Mitthu") :: List(Dog("Bruno"))   // :: widens via a lower bound
println(loudest(List(Dog("Bruno"), Dog("Sheru"))))

A quick variance rule

If a type only produces values of A (returns them), it can be covariant. If it only consumes A (takes it as input), it can be contravariant. If it does both, as mutable collections do, keep it invariant.

त्वरित जाँच: Why is Array[A] invariant in Scala?

  • Because arrays are mutable: a covariant array would allow writing a Cat into an Array[Dog]
  • Arrays cannot hold objects
  • Because of performance
  • It is covariant
Answer

Because arrays are mutable: a covariant array would allow writing a Cat into an Array[Dog] — Mutable containers both produce and consume elements, so covariance would be unsafe.