Generics
Prerequisites: Traits
Generics let classes and methods work with any type, while keeping
full type checking. You've already used them: List[Int],
Option[String], Map[String, Int].
Generic methods
def firstOrElse[A](items: List[A], default: A): A =
items.headOption.getOrElse(default)
def pairUp[A, B](as: List[A], bs: List[B]): List[(A, B)] = as.zip(bs)
@main def genericMethods(): Unit =
println(firstOrElse(List(3, 4), 0))
println(firstOrElse(List.empty[String], "none"))
println(pairUp(List("a", "b"), List(1, 2)))
3
none
List((a,1), (b,2))
[A] declares a type parameter. The compiler infers A from the
arguments: Int in the first call, String in the second.
Generic classes
case class Box[A](value: A):
def map[B](f: A => B): Box[B] = Box(f(value))
class Stack[A]:
private var items: List[A] = Nil
def push(item: A): Unit = items = item :: items
def pop(): Option[A] =
val top = items.headOption
items = items.drop(1)
top
@main def genericClasses(): Unit =
println(Box(21).map(_ * 2))
println(Box("scala").map(_.length))
val stack = Stack[String]()
stack.push("a")
stack.push("b")
println(stack.pop())
println(stack.pop())
println(stack.pop())
Box(42)
Box(5)
Some(b)
Some(a)
None
Bounds
Upper bounds (A <: Shape) require a type parameter to be a subtype of
something, so you can use its members:
trait Animal:
def name: String
def sound: String
case class Dog(name: String) extends Animal:
def sound = "Woof"
case class Cat(name: String) extends Animal:
def sound = "Meow"
def loudest[A <: Animal](animals: List[A]): A = animals.maxBy(_.sound.length)
@main def bounds(): Unit =
val dogs = List(Dog("Rex"), Dog("Fido"))
val winner: Dog = loudest(dogs)
println(winner)
println(s"${Cat("Tom").name} says ${Cat("Tom").sound}")
Dog(Rex)
Tom says Meow
The result type is Dog, not just Animal — the bound lets loudest
call sound while still returning the precise type it was given.
Level 3 covers variance (+A, -A) and higher-kinded types.