पाठ 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.
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.