Free Handbook · Every example compiled & verified

Generics & Givens

Type parameters, variance and bounds, extension methods, and type classes built from traits, given instances, using clauses and context bounds.

0 / 142 lessons🔥 0 day streak
ShareXLinkedIn

Module 09 · what you'll be able to do

  • Write generic classes and methods with type parameters and let the compiler infer them
  • Explain covariance, contravariance and invariance, and read the compiler errors they produce
  • Constrain type parameters with upper and lower bounds
  • Add methods to existing types with extension methods
  • Build a type class with a trait, given instances and using clauses, and use context bounds such as [A: Ordering]
01

Type parameters on classes and methods

A type parameter lets one piece of code work for many types while staying fully type-checked. It goes in square brackets: class Box[A], def first[A](xs: List[A]): A. A is a placeholder the caller fills in — usually without writing it, because the compiler infers it from the arguments. You have used generic types since Module 06: List[Int] is List with A = Int.

scalaMain.scala
case class Box[A](value: A):
  def map[B](f: A => B): Box[B] = Box(f(value))

case class Pair[A, B](first: A, second: B):
  def swap: Pair[B, A] = Pair(second, first)

def firstOr[A](xs: List[A], default: A): A =
  xs.headOption.getOrElse(default)

@main def run(): Unit =
  val b = Box(21)
  println(b.map(_ * 2))
  println(b.map(n => s"#$n"))

  println(Pair("id", 7).swap)

  println(firstOr(List(3, 4), 0))
  println(firstOr(List.empty[String], "none"))
  println(firstOr[Double](Nil, 1.5))
Outputcompiled & run with real Scala
Box(42)
Box(#21)
Pair(7,id)
3
none
1.5

Nowhere did we write Box[Int] — the compiler inferred it from 21, and inferred B = String from the lambda in the second map. You can still pass type arguments explicitly, as the last line does, when there is nothing to infer from.

Your turn

Add a method def zip[B](other: Box[B]): Box[(A, B)] to Box and print Box(1).zip(Box("a")).

Type erasure
On the JVM, type arguments exist only at compile time. At runtime a List[Int] and a List[String] are both just List, so a pattern like case xs: List[Int] cannot really check the element type — the compiler warns you when you try. Match on the elements instead.
02

Variance: +A, -A and invariance

If a Dog is an Animal, is a Box[Dog] a Box[Animal]? Variance is the answer, and you choose it with a mark on the type parameter:

DeclarationNameMeaningStandard examples
class Box[A]InvariantBox[Dog] and Box[Animal] are unrelatedArray, mutable.ArrayBuffer
class Box[+A]CovariantBox[Dog] is a Box[Animal]List, Vector, Option, Either
class Box[-A]ContravariantBox[Animal] is a Box[Dog]function parameters, Ordering-like consumers

Rule of thumb: a type that only produces As (you read them out) can be covariant. A type that only consumes As (you pass them in) can be contravariant. A type that does both — anything mutable — must stay invariant. A function A => B is both at once: contravariant in what it takes, covariant in what it returns.

scalaMain.scala
class Animal(val name: String)
class Dog(name: String) extends Animal(name)

trait Source[+A]:
  def next(): A

trait Printer[-A]:
  def print(a: A): String

@main def run(): Unit =
  val dogs: List[Dog] = List(Dog("Rex"), Dog("Bit"))
  val animals: List[Animal] = dogs
  println(animals.map(_.name))

  val dogSource: Source[Dog] = () => Dog("Max")
  val animalSource: Source[Animal] = dogSource
  println(animalSource.next().name)

  val animalPrinter: Printer[Animal] = a => s"animal ${a.name}"
  val dogPrinter: Printer[Dog] = animalPrinter
  println(dogPrinter.print(Dog("Rex")))
Outputcompiled & run with real Scala
List(Rex, Bit)
Max
animal Rex

A list of dogs can be used where a list of animals is expected, because List is covariant. A printer that can print any animal can certainly print a dog, so Printer[Animal] works as a Printer[Dog] — contravariance runs the other way. The lambdas are accepted for Source and Printer because each trait has exactly one abstract method.

Error you will hit

An invariant class does not follow subtyping

scala
class Animal(val name: String)
class Dog(name: String) extends Animal(name)

class Cage[A](var occupant: A)

def describe(c: Cage[Animal]): String = c.occupant.name

@main def run(): Unit =
  val c = Cage(Dog("Rex"))
  println(describe(c))
-- [E007] Type Mismatch Error: Main.scala:10:19
10 |  println(describe(c))
   |                   ^
   |                   Found:    (c : Cage[Dog])
   |                   Required: Cage[Animal]
1 error found
Compilation failed
Why the compiler said that

Cage has no variance mark, so it is invariant: Cage[Dog] is not a Cage[Animal]. That is correct here. If it were allowed, describe could do c.occupant = Animal("Cat") and your dog cage would now hold a cat.

The fix

Make the method generic with an upper bound (next lesson), so it accepts a cage of any kind of animal without being able to break it.

scala
class Animal(val name: String)
class Dog(name: String) extends Animal(name)

class Cage[A](var occupant: A)

def describe[A <: Animal](c: Cage[A]): String = c.occupant.name

@main def run(): Unit =
  val c = Cage(Dog("Rex"))
  println(describe(c))
Error you will hit

A covariant type parameter in a mutable field

scala
class Cage[+A](var occupant: A)

@main def run(): Unit =
  val c = Cage(42)
  println(c.occupant)
-- Error: Main.scala:1:19
1 |class Cage[+A](var occupant: A)
  |               ^^^^^^^^^^^^^^^
  |covariant type A occurs in invariant position in type A of variable occupant
1 error found
Compilation failed
Why the compiler said that

A var field is both read (a getter that produces an A) and written (a setter occupant_=(a: A) that consumes one), so its type sits in an invariant position. If Cage were covariant, a Cage[Int] could be treated as a Cage[Any] and have a String stored in it. The compiler checks every place A appears and rejects the class.

The fix

Make the field a val (read-only data can be covariant), or drop the + and keep the class invariant.

scala
class Cage[+A](val occupant: A)

@main def run(): Unit =
  val c = Cage(42)
  println(c.occupant)
03

Upper and lower bounds

A bound restricts which types a parameter accepts. An upper bound A <: Animal means "A must be Animal or a subtype", which lets the method use everything Animal has while still returning the precise type the caller passed in. A lower bound B >: A means "B must be A or a supertype" — it is how covariant collections still manage to accept new elements.

scalaMain.scala
trait Entity:
  def id: Int

case class User(id: Int, name: String) extends Entity
case class Product(id: Int, title: String) extends Entity

def byId[A <: Entity](items: List[A], id: Int): Option[A] =
  items.find(_.id == id)

@main def run(): Unit =
  val users = List(User(1, "Asha"), User(2, "Ravi"))
  val u: Option[User] = byId(users, 2)
  println(u.map(_.name))

  val products = List(Product(7, "Kettle"))
  println(byId(products, 7))
Outputcompiled & run with real Scala
Some(Ravi)
Some(Product(7,Kettle))

Because of the bound, byId may call _.id. And because it returns Option[A], not Option[Entity], the caller gets a User back and can read name without a cast.

scalaMain.scala
sealed trait Stack[+A]:
  def push[B >: A](b: B): Stack[B] = NonEmpty(b, this)

case object Empty extends Stack[Nothing]
case class NonEmpty[+A](top: A, rest: Stack[A]) extends Stack[A]

class Fruit(val name: String):
  override def toString: String = name
class Apple extends Fruit("apple")
class Pear extends Fruit("pear")

@main def run(): Unit =
  val apples: Stack[Apple] = Empty.push(Apple())
  val fruits: Stack[Fruit] = apples.push(Pear())
  println(fruits)
Outputcompiled & run with real Scala
NonEmpty(pear,NonEmpty(apple,Empty))

push cannot take an A in a covariant class (that is a consumer position), so it takes any B that is a supertype of A and returns a Stack[B]. Pushing a pear onto apples gives a stack of fruit. List's :: is declared exactly this way, and Empty is a Stack[Nothing] — Nothing is a subtype of every type, just like Nil is a List[Nothing].

04

Extension methods

An extension method adds a method to a type you do not own — String, Int, a library class — without subclassing or wrapping it. You write extension (x: Type) followed by one or more ordinary defs; inside them, x is the value the method was called on. Callers then use normal dot syntax. Module 04 used one to give an opaque type a value method.

scalaMain.scala
extension (s: String)
  def isPalindrome: Boolean =
    val clean = s.toLowerCase.filter(_.isLetter)
    clean == clean.reverse
  def words: List[String] = s.split("\\s+").toList.filter(_.nonEmpty)
  def truncate(n: Int): String = if s.length <= n then s else s.take(n) + "..."

extension (n: Int)
  def isEven: Boolean = n % 2 == 0
  def times(action: => Unit): Unit = for _ <- 1 to n do action

@main def run(): Unit =
  println("Never odd or even".isPalindrome)
  println("  handbooks   are fun ".words)
  println("Extension methods".truncate(9))
  println(10.isEven)
  3.times(print("hi "))
  println()
Outputcompiled & run with real Scala
true
List(handbooks, are, fun)
Extension...
true
hi hi hi

Several methods can share one extension clause. times takes a by-name block, so 3.times(...) reads like a loop keyword. Under the hood the call "abc".truncate(2) is simply truncate("abc")(2), a static method call — no object is allocated.

Your turn

Add extension (xs: List[Int]) def average: Double that returns 0.0 for an empty list, and print List(2, 4, 9).average.

Extensions can be generic, and can target a specific shape of a generic type such as List[Int] or any Option[A]. They are only visible where they are in scope, so libraries put them in an object and you import the ones you want.

scalaMain.scala
object CollectionSyntax:
  extension [A](xs: List[A])
    def second: Option[A] = xs.drop(1).headOption
    def countWhere(p: A => Boolean): Int = xs.count(p)

  extension (xs: List[Int])
    def total: Int = xs.sum

  extension [A](opt: Option[A])
    def orFail(msg: String): Either[String, A] = opt.toRight(msg)

@main def run(): Unit =
  import CollectionSyntax.*
  val xs = List(5, 8, 13)
  println(xs.second)
  println(xs.countWhere(_ > 6))
  println(xs.total)
  println(List("a").second.orFail("no second element"))
Outputcompiled & run with real Scala
Some(8)
2
26
Left(no second element)

total exists only on List[Int]; calling it on a List[String] would not compile. Without the import, none of these methods would be found.

05

Type classes: trait, given and using

Suppose you want a show method that turns values into user-facing text, for Int, for Boolean, for your own case classes — and for lists of any of those. You cannot add a parent trait to Int. A type class solves this in three parts:

  1. 1
    The interface

    A generic trait Show[A] with the operations, taking the value as a parameter.

  2. 2
    Instances

    A given for each type that supports it: given Show[Int] with .... The compiler records these in a table of "given" values, one per type.

  3. 3
    Using it

    Functions ask for an instance with a using parameter list. The caller does not pass it: the compiler finds the matching given by type and passes it automatically.

scalaMain.scala
trait Show[A]:
  def show(a: A): String

case class Money(cents: Long)

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

given Show[Boolean] with
  def show(b: Boolean): String = if b then "yes" else "no"

given Show[Money] with
  def show(m: Money): String = f"Rs ${m.cents / 100.0}%.2f"

given [A](using s: Show[A]): Show[List[A]] with
  def show(xs: List[A]): String = xs.map(s.show).mkString("[", ", ", "]")

def display[A](label: String, a: A)(using s: Show[A]): Unit =
  println(s"$label: ${s.show(a)}")

@main def run(): Unit =
  display("count", 3)
  display("active", true)
  display("price", Money(12950))
  display("prices", List(Money(100), Money(250)))
  display("flags", List(true, false))
Outputcompiled & run with real Scala
count: 3
active: yes
price: Rs 129.50
prices: [Rs 1.00, Rs 2.50]
flags: [yes, no]

The fourth given is the powerful one: "if you can show an A, I can show a List[A]". The compiler builds Show[List[Money]] out of Show[Money] for you. Adding support for a new type means adding one given; no existing code changes.

Your turn

Add a given Show[String] that wraps the text in quotes, then call display("names", List("Asha", "Ravi")).

VisualizeHow the compiler finds Show[List[Money]]Step 1 / 6
given Show[Money] with
def show(m: Money): String = f"Rs ${m.cents / 100.0}%.2f"
given [A](using s: Show[A]): Show[List[A]] with
def show(xs: List[A]): String = xs.map(s.show).mkString("[", ", ", "]")
def display[A](label: String, a: A)(using s: Show[A]): Unit =
println(s"$label: ${s.show(a)}")
display("prices", List(Money(100), Money(250)))
Line 10

The argument is a List[Money], so the compiler infers A = List[Money] and needs a Show[List[Money]] for the using clause.

Variables now
AList[Money]
All 6 steps as a table
StepLineWhat happenedVariables now
110The argument is a List[Money], so the compiler infers A = List[Money] and needs a Show[List[Money]] for the using clause.A = List[Money]
24No given has exactly that type, but this generic given produces a Show[List[A]] for any A — with A = Money — provided it can get a Show[Money].needed = Show[Money]
31The Show[Money] given matches. The compiler now has everything and writes the call for you.found = Show[Money]
410At compile time the call has become, roughly, display("prices", list)(using listShow(using moneyShow)). Nothing is searched at runtime.
55At runtime the list instance maps moneyShow.show over the elements and joins them.
68The label and text are printed.

summon[Show[Int]] fetches the instance the compiler would pass — handy for calling it directly. Combined with an extension method inside the trait, it gives the type class ordinary method syntax: money.show. Put instances for your own types in the type's companion object, and the compiler finds them anywhere without an import.

scalaMain.scala
trait Show[A]:
  extension (a: A) def show: String

case class Point(x: Int, y: Int)
object Point:
  given Show[Point] with
    extension (p: Point) def show: String = s"(${p.x}, ${p.y})"

given Show[Int] with
  extension (n: Int) def show: String = s"int $n"

@main def run(): Unit =
  println(Point(2, 3).show)
  println(5.show)
  val s = summon[Show[Point]]
  println(s.show(Point(0, 0)) == Point(0, 0).show)
Outputcompiled & run with real Scala
(2, 3)
int 5
true

Point's instance lives in its companion, so Point(2, 3).show works with no import at all. This is the shape real libraries use for JSON encoders, database mappers and orderings.

Error you will hit

No given instance for a type

scala
trait Show[A]:
  def show(a: A): String

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

def display[A](a: A)(using s: Show[A]): String = s.show(a)

@main def run(): Unit =
  println(display(42))
  println(display(3.5))
-- [E172] Type Error: Main.scala:11:22
11 |  println(display(3.5))
   |                      ^
   |No given instance of type Show[Double] was found for parameter s of method display
1 error found
Compilation failed
Why the compiler said that

The using clause asks for a Show[Double], and the compiler searched the local scope, the imports and the companions of Show and Double without finding one. This is a compile-time error: a missing instance can never become a runtime surprise.

The fix

Provide the instance (or import the object that defines it).

scala
trait Show[A]:
  def show(a: A): String

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

given Show[Double] with
  def show(d: Double): String = f"$d%.1f"

def display[A](a: A)(using s: Show[A]): String = s.show(a)

@main def run(): Unit =
  println(display(42))
  println(display(3.5))
06

Context bounds: [A: Ordering]

Writing (using ord: Ordering[A]) on every generic method gets noisy. A context bound says the same thing in the type parameter list: def maxOf[A: Ordering](xs: List[A]) means "there must be a given Ordering[A]". Inside, you summon it when you need it. The standard library's sorted, max and sum are built exactly this way on Ordering and Numeric.

scalaMain.scala
def maxOf[A: Ordering](xs: List[A]): Option[A] =
  xs.reduceOption((a, b) => if summon[Ordering[A]].gt(a, b) then a else b)

def total[A: Numeric](xs: List[A]): A =
  xs.foldLeft(summon[Numeric[A]].zero)(summon[Numeric[A]].plus)

case class Release(major: Int, minor: Int)
object Release:
  given Ordering[Release] = Ordering.by(r => (r.major, r.minor))

@main def run(): Unit =
  println(maxOf(List(3, 9, 2)))
  println(maxOf(List("pear", "apple")))
  println(total(List(1.5, 2.25)))
  println(total(List(BigInt(10), BigInt(20))))

  val releases = List(Release(2, 13), Release(3, 1), Release(2, 9))
  println(releases.sorted)
  println(maxOf(releases))
  println(releases.sorted(using Ordering[Release].reverse).head)
Outputcompiled & run with real Scala
Some(9)
Some(pear)
3.75
30
List(Release(2,9), Release(2,13), Release(3,1))
Some(Release(3,1))
Release(3,1)

Ordering.by builds an ordering from a key — here a tuple, which compares field by field. Once Release has a given Ordering in its companion, sorted, max and our own maxOf all work on it. using at the call site passes an instance explicitly, overriding the given one.

Your turn

Sort the releases newest-first with sortBy(r => (-r.major, -r.minor)) and compare the result with the reverse ordering.

Error you will hit

sorted on a type with no Ordering

scala
case class Release(major: Int, minor: Int)

@main def run(): Unit =
  val rs = List(Release(3, 1), Release(2, 9))
  println(rs.sorted)
-- [E172] Type Error: Main.scala:5:19
5 |  println(rs.sorted)
  |                   ^
  |No given instance of type Ordering[B] was found for parameter ord of method sorted in trait StrictOptimizedSeqOps.
  |I found:
  |
  |    scala.math.Ordering.ordered[B](
  |      /* missing */summon[scala.math.Ordering.AsComparable[B]])
  |
  |But no implicit values were found that match type scala.math.Ordering.AsComparable[B]
  |
  |where:    B is a type variable with constraint >: Release
  |.
  |
  |The following import might make progress towards fixing the problem:
  |
  |  import scala.math.Ordered.orderingToOrdered
1 error found
Compilation failed
Why the compiler said that

sorted has a context bound on Ordering. Numbers and strings have a given Ordering in the standard library, but the compiler has no idea how two Releases compare — by major? by minor? — so it will not guess. The message says Ordering[B] because sorted is declared as sorted[B >: A]; the suggested import is a red herring here.

The fix

Sort by a key that already has an ordering, or define a given Ordering[Release] in the companion as in the example above.

scala
case class Release(major: Int, minor: Int)

@main def run(): Unit =
  val rs = List(Release(3, 1), Release(2, 9))
  println(rs.sortBy(r => (r.major, r.minor)))
07

derives: instances generated for you

Writing a given by hand for every case class gets repetitive. A derives clause asks the type class to generate the instance from the shape of the type: case class User(name: String, age: Int) derives Codec. The type class must support derivation, which libraries do for JSON codecs, equality, ordering and more. The standard library's built-in example is CanEqual, which works with strict equality to stop comparisons between unrelated types.

scalaMain.scala
import scala.language.strictEquality

enum Status derives CanEqual:
  case Active, Suspended

case class UserId(n: Int) derives CanEqual

@main def run(): Unit =
  val s = Status.Active
  println(s == Status.Suspended)
  println(UserId(7) == UserId(7))
Outputcompiled & run with real Scala
false
true

Under strictEquality, == only compiles when a CanEqual instance exists for the two types. derives CanEqual generates one for comparing a type with itself, so UserId(7) == 7 — a classic bug that normal == accepts and always answers false — becomes a compile error.

scala
// with a JSON library such as circe or upickle on the classpath
case class Order(id: Int, item: String, qty: Int) derives Codec

val json = Order(1, "tea", 3).asJson   // encoder generated from the fields

Not runnable here (it needs a library), but this is the most common derives you will meet at work.

Type parameter
A placeholder type in square brackets, such as A in List[A], filled in by the caller or inferred.
Invariant
The default: Box[Dog] and Box[Animal] are unrelated types.
Covariant (+A)
Box[Dog] is a subtype of Box[Animal]; allowed when A is only produced, never consumed.
Contravariant (-A)
Box[Animal] is a subtype of Box[Dog]; allowed when A is only consumed.
Upper bound (A <: T)
A must be T or a subtype of T.
Lower bound (B >: A)
B must be A or a supertype of A; used to add elements to covariant types.
Extension method
A method added to an existing type from outside it, called with ordinary dot syntax.
Type class
A generic trait describing a capability, plus given instances that give specific types that capability.
given
A value the compiler can pass automatically to using parameters of its type.
using clause
A parameter list the compiler fills in with a matching given instance.
summon
Fetches the given instance of a type, as in summon[Ordering[Int]].
Context bound
Shorthand [A: TC] for a using parameter of type TC[A].
derives
A clause that asks a type class to generate its instance for a type automatically.
Quick check

Why can List[+A] be covariant but Array[A] cannot?

Quick check

What does def sortDesc[A: Ordering](xs: List[A]) require?

Frequently asked questions

What are givens and using clauses in Scala 3?
A given declares a value the compiler may pass automatically, and a using clause declares a parameter the compiler should fill with a matching given by type. Together they replace Scala 2's implicit values and parameters, and they are how type classes such as Ordering and JSON codecs are supplied.
What is the difference between covariance and contravariance?
With covariance (+A), a Box[Dog] can be used as a Box[Animal]; it is safe for types that only give out values, like immutable List. With contravariance (-A), a Handler[Animal] can be used as a Handler[Dog]; it is safe for types that only take values in, like function parameters.
How do extension methods work in Scala 3?
You write extension (x: SomeType) def newMethod(...) = .... Wherever the extension is in scope, x.newMethod(...) compiles to a plain call of newMethod(x)(...). They replace Scala 2's implicit classes and are also how type classes offer method syntax.

Finish the Scala handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.