scalaadvanced

Variance and Type Bounds

Understand covariance, contravariance, and type bounds: upper bounds, lower bounds, and context bounds.

scala
// Covariance (+T): if A <: B then F[A] <: F[B]
class Box[+T](val value: T):
  // Can only use T in "out" position (return types)
  def get: T = value
  // Cannot: def set(v: T) — T in "in" position

// Contravariance (-T): if A <: B then F[B] <: F[A]
trait Printer[-T]:
  def print(value: T): Unit

class AnimalPrinter extends Printer[Animal]:
  def print(value: Animal): Unit = println(s"Animal: ${value.name}")

class CatPrinter extends Printer[Cat]:
  def print(value: Cat): Unit = println(s"Cat: ${value.name}, lives: ${value.lives}")

trait Animal:
  def name: String

case class Cat(name: String, lives: Int) extends Animal
case class Dog(name: String, breed: String) extends Animal

// Upper bound (<:): T must be subtype of Bound
def findMax[T <: Comparable[T]](items: List[T]): T =
  items.reduce((a, b) => if a.compareTo(b) >= 0 then a else b)

// Lower bound (>:): T must be supertype of Bound
class Stack[+T]:
  private val items: List[T] = Nil
  // push needs lower bound because T is covariant
  def push[U >: T](item: U): Stack[U] = new Stack[U]:
    override val items: List[U] = item :: Stack.this.items
  def peek: Option[T] = items.headOption
  def toList: List[T] = items

// Context bounds (: TypeClass)
def sorted[T: Ordering](items: List[T]): List[T] = items.sorted
def show[T: Show](value: T): String = summon[Show[T]].show(value)

trait Show[T]:
  def show(value: T): String

given Show[Int] with
  def show(value: Int) = s"Int($value)"

given Show[String] with
  def show(value: String) = s"Str($value)"

// Multiple bounds
def process[T: Ordering: Show](items: List[T]): String =
  val s = summon[Show[T]]
  items.sorted.map(s.show).mkString(", ")

// F-bounded polymorphism
trait Builder[Self <: Builder[Self]]:
  def addStep(step: String): Self
  def build: String

class PipelineBuilder extends Builder[PipelineBuilder]:
  private val steps = List.empty[String]
  def addStep(step: String): PipelineBuilder =
    val b = PipelineBuilder()
    b
  def build: String = steps.mkString(" -> ")

// Variance in practice: Function1[-T, +R]
// Input types are contravariant, output types are covariant
val animalToString: Animal => String = a => a.name
val catToString: Cat => String = animalToString  // OK! contravariant input

val catFactory: () => Cat = () => Cat("Whiskers", 9)
val animalFactory: () => Animal = catFactory  // OK! covariant output

@main def run(): Unit =
  // Covariance
  val catBox: Box[Cat] = Box(Cat("Felix", 7))
  val animalBox: Box[Animal] = catBox  // OK because Box is covariant
  println(s"Animal box: ${animalBox.get.name}")

  // Contravariance
  val animalPrinter: Printer[Animal] = AnimalPrinter()
  val catPrinter: Printer[Cat] = animalPrinter  // OK! Printer is contravariant
  catPrinter.print(Cat("Luna", 9))

  // Type bounds
  println(s"\nCat name: ${catToString(Cat("Milo", 5))}")
  println(s"Animal factory: ${animalFactory().name}")

  // Context bounds
  println(s"\nSorted: ${sorted(List(3, 1, 4, 1, 5))}")
  println(s"Show: ${show(42)}")
  println(s"Process: ${process(List(3, 1, 2))}")

Use Cases

  • Type-safe generic containers
  • Flexible API design
  • Understanding subtyping relationships

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.