Skip to main content
Scala: Variance
  1. Posts/

Scala: Variance

·1005 words·5 mins·
Roman
Author
Roman
Photographer with MSci in Computer Science and a Home Lab obsession
Table of Contents

TLDR: Why Variance Matters
#

When working with generic types like List[T], Option[T], or your own Container[T], variance becomes crucial.

If Dog extends Animal, what about List[Dog] and List[Animal]?

Jump to:

Variance
#

Basic Setup
#

Let’s start with a simple class hierarchy:

class Book
class Textbook(subject: String) extends Book
class Novel(title: String) extends Book

val mathBook = new Textbook("Mathematics")
val mysteryNovel = new Novel("The Great Mystery")
val aBook: Book = mathBook 
// Textbook is a subtype of Book
classDiagram
    Book <|-- Textbook : extends
    Book <|-- Novel : extends
    class Book
    class Textbook {
        +String subject
    }
    class Novel {
        +String title
    }

Subtype Relations Notation
#

  • A <: B means “A is a subtype of B” (A extends B)
  • A >: B means “A is a supertype of B” (B extends A)

If Textbook <: Book, what about Library[Textbook] vs Library[Book]?

The Three Types of Variance
#

1. Covariance (+)
#

Covariance means if A <: B, then Container[A] <: Container[B].

// Defining a covariant type
class Library[+T] // + makes T covariant

val someBooks: Library[Book] = new Library[Textbook]
// Library[Textbook] <: Library[Book] works!

Covariance (+T) - Producers: “More Specific Outputs”

  • Output values of type T (like List[+T], Option[+T])
  • Library[Textbook] can be used wherever Library[Book] is expected
  • Safe because every Textbook is a Book

2. Invariance (no symbol)
#

Invariance means no subtype relationship between parameterized types.

trait BookExchange[T] {
  def trade(bookToGive: T): T
  // both accepts AND returns T
}

// Not allowed: BookExchange[Textbook] as BookExchange[Book]
// Would break type safety if you traded a Novel for a Textbook

Why invariance is necessary: If we allowed BookExchange[Textbook] <: BookExchange[Book], you could:

  1. Pass it where BookExchange[Book] is expected
  2. Trade a Novel (which is a Book)
  3. Get back a Novel where you expected a Textbook

3. Contravariance (-)
#

Contravariance means if A <: B, then Container[B] <: Container[A] (flipped!).

// Example: A Librarian that can organize any Book 
// can also organize Textbooks specifically
trait Librarian[-T] { // - makes T contravariant
  def organize(book: T): Boolean
}

// Textbook <: Book, so Librarian[Book] <: Librarian[Textbook]
val myLibrarian: Librarian[Textbook] = new Librarian[Book] {
  def organize(book: Book): Boolean = true
}

val organizingMathBook = myLibrarian.organize(mathBook)

Contravariance (-T) - Consumers: “More General Inputs”

  • Accept values of type T (like Comparator[-T])
  • Librarian[Book] can be used wherever Librarian[Textbook] is expected
  • Safe because it can handle any Book (including Textbook)

Variance Summary
#

Type Notation Relationship Use Case
Covariance [+A] If A <: B, then Container[A] <: Container[B] Producers (output values)
Contravariance [-A] If A <: B, then Container[A] >: Container[B] Consumers (input values)
Invariance [A] No relationship between containers Both produce and consume

Variance Positions
#

Scala enforces rules about where variant types can appear to maintain type safety:

Issue Concepts
#

Covariant Position Issues
#

Types of val fields are in covariant positions because they can only be read (produce values). Since val fields return values, they must preserve the type relationship safely.

// ❌ This doesn't compile!
class Librarian[-T](val favoriteBook: T) 
// Error: Contravariant type T occurs in covariant position

// Let's assume the above compiled (hypothetically)
// Here's why this breaks type safety:
val bookLibrarian: Librarian[Book] = 
    new Librarian[Book](new Novel("Holmes"))
val textbookLibrarian: Librarian[Textbook] = bookLibrarian 
// contravariance allows this ↑
val book: Textbook = textbookLibrarian.favoriteBook
//  Runtime error: Novel  Textbook!

The Problem: Contravariant types (-T) can’t be used in positions where values are returned (covariant positions like val fields). This would let you extract the wrong type, breaking type safety.

Contravariant Position Issues
#

Types of var fields are in contravariant positions because they can be written to (accept values). Since var fields accept input through setters, they must handle the type relationship in the opposite direction.

// ❌ This doesn't compile!
class MutableLibrary[+T](var content: T)
// Error: Covariant type T occurs in contravariant position

// Let's assume the above compiled (hypothetically)
// Here's why this breaks type safety:
val textbookLibrary: MutableLibrary[Textbook] = 
    new MutableLibrary[Textbook](new Textbook("Math"))
val bookLibrary: MutableLibrary[Book] = textbookLibrary 
// covariance allows this ↑
bookLibrary.content = new Novel("Mystery Story")
//  Runtime error: Novel assigned where Textbook expected!

The Problem: Covariant types (+T) can’t be used in positions where values are accepted (contravariant positions like var setters). This would allow unsafe mutations that violate type constraints.

Practical Examples
#

Method Arguments (Contravariant Position)
#

Method parameters accept input values, placing them in contravariant positions. Covariant types (+T) can’t appear here as it would break type safety.

// ❌ This doesn't compile!
class MyLibrary[+T] {
  def add(book: T): MyLibrary[T] = new MyLibrary[T]
  // Error: Covariant type T occurs in contravariant position
}

// ✅ Solution: Widen the type
class MyLibrary[+A] {
  def add[B >: A](book: B): MyLibrary[B] = new MyLibrary[B]
  // B is a supertype of A, so we can accept more general types
  // This preserves covariance while allowing safe input
}

Why this works: A MyLibrary[Textbook] can now accept any Book (including Novel), creating a MyLibrary[Book]. Since we’re widening to a more general type, covariance is preserved safely.

Method Return Types (Covariant Position)
#

Method return types produce output values, placing them in covariant positions. Contravariant types (-T) can’t appear here as it would break type safety.

// ❌ This doesn't compile!
abstract class Librarian[-T] {
  def recommendBook(): T
  // Error: Contravariant type T occurs in covariant position
}

// ✅ Solution: Narrow the type argument
abstract class Librarian[-A] {
  def recommendBook[B <: A](): B
  // B is a subtype of A, so we return more specific types
  // This preserves contravariance while allowing safe output
}

Why this works: A Librarian[Book] can recommend a specific Textbook. Since we’re narrowing to a more specific type, the librarian can still handle any Book while returning something more precise.

Variance Position Summary
#

Variance Type Can’t Appear In Solution Example
Covariant (+T) Method parameters, var setters Use [B >: T] (supertype bound) def add[B >: A](item: B)
Contravariant (-T) val fields, method return types Use [B <: T] (subtype bound) def get[B <: A](): B

References
#