Generics, variance and type bounds
Parameterise types, set upper and lower bounds, and get variance right so a subtype relationship survives inside a container.
Type parameters and bounds
// an unbounded parameter: any type works
def first[A](xs: List[A]): Option[A] = xs.headOption
// an upper bound: A must be comparable to itself
def maxOf[A <: Comparable[A]](xs: List[A]): Option[A] =
xs.reduceOption((a, b) => if a.compareTo(b) >= 0 then a else b)
// a context bound: an Ordering[A] must be available
def sorted[A: Ordering](xs: List[A]): List[A] = xs.sorted
// equivalent, spelled out
def sorted2[A](xs: List[A])(using ord: Ordering[A]): List[A] = xs.sorted
// a lower bound: the result type must be a supertype of both
def cons[A >: Nothing](head: A, tail: List[A]): List[A] = head :: tail
// a type member instead of a parameter, for a companion-style API
trait Repository:
type Id
def find(id: Id): Option[String]
class UserRepository extends Repository:
type Id = java.util.UUID
def find(id: Id): Option[String] = None| Syntax | Name | Meaning |
|---|---|---|
[A] | Type parameter | Any type, including Nothing |
[A <: Bound] | Upper bound | A must be a subtype of Bound |
[A >: Bound] | Lower bound | A must be a supertype of Bound |
[A: TC] | Context bound | A TC[A] instance must be available |
[A: TC as a] | Named context bound | Scala 3: name the instance for reuse |
[A & B] | Intersection | A with both capabilities |
[A | B] | Union | A or B |
Variance
Variance answers one question: if a Dog is an Animal, is a List[Dog] a List[Animal]? Scala makes you answer it in the declaration, and the compiler checks that your answer is safe.
class Animal
class Dog extends Animal
class Puppy extends Dog
// covariant: a producer. List[+A] means List[Dog] IS a List[Animal]
sealed trait Producer[+A]:
def get: A
// contravariant: a consumer. Consumer[Animal] IS a Consumer[Dog]
sealed trait Consumer[-A]:
def accept(a: A): Unit
// invariant: both roles, so no subtyping either way
class Box[A](var value: A)
val p: Producer[Animal] = new Producer[Dog] { def get = new Dog }
val c: Consumer[Dog] = new Consumer[Animal] { def accept(a: Animal) = () }
// this is what makes variance rules: a covariant parameter may only appear
// in an output position, otherwise the compiler rejects the declaration
// class Bad[+A](var value: A) // error: covariant type A occurs in a contravariant position
def printAll[A](xs: List[A]): Unit = xs.foreach(println)
printAll(List(new Dog, new Puppy)) // List[Puppy] works where List[Dog] is expected
// a wildcard: accept any element type
def size(xs: List[?]): Int = xs.size
def total(xs: List[? <: Number]): Double = xs.map(_.doubleValue).sum- Read the question as "who supplies and who consumes". A value you only return can be covariant; a value you only accept can be contravariant.
Function1[-T, +R]is contravariant in the argument and covariant in the result, which is exactly the intuition above.- Mutable state forces invariance.
Arrayis invariant because a write would otherwise be unsound. ?is Scala 3's wildcard for an existential type;_still works but is on the way out.
Type lambdas and self-referential types
// a self-referential F-bounded type: compare with the concrete subtype
trait Comparable[A <: Comparable[A]]:
def compareTo(other: A): Int
final case class Version(major: Int, minor: Int) extends Comparable[Version]:
def compareTo(other: Version): Int =
(major, minor).compareTo((other.major, other.minor))
// a type lambda: fix one parameter of a two-parameter type
type IntMap[V] = Map[Int, V] // Scala 3 alias, the common case
type EitherString[A] = Either[String, A]
def lookup[F[_], A](fa: F[A]): F[A] = fa // F is a type constructor
val r: EitherString[Int] = lookup[EitherString, Int](Right(1))
// match types let the compiler compute a type from another type
type Elem[X] = X match
case String => Char
case Array[t] => t
case List[t] => t
val c: Elem[String] = 'a'
val i: Elem[List[Int]] = 1💡
Variance is a declaration-site property in Scala, unlike Java's use-site wildcards. If the compiler rejects your variance annotation, it has found a real hole: writing through a covariant reference, or reading through a contravariant one, would break type safety at run time.
FAQ
Why does the compiler say my covariant type occurs in a contravariant position?
A parameter of a method is an input, so a covariant
A cannot appear there. Either make the member private or protected, or add a lower bound such as def add[B >: A](b: B), which is the standard workaround.What does Nothing mean?
Nothing is a subtype of every type and has no values. It types expressions that never return, like throw, which is why Left in a comprehension or Nil for a list fits anywhere.Related
Givens, implicits and type classes Traits, classes and object-oriented Scala
Last refreshed 2026-09-18.