Scala Generics
Generics let you write code that works with any type, not just one specific type. Instead of writing a separate function for integers and another for strings, you write one generic function that handles both — and any other type you throw at it. This is one of the most powerful tools in Scala's type system.
The Problem Without Generics
// Without generics — you need a separate function for each type
def firstInt(list: List[Int]): Int = list.head
def firstString(list: List[String]): String = list.head
def firstDouble(list: List[Double]): Double = list.head
// With generics — one function handles them all
def first[A](list: List[A]): A = list.head
first(List(1, 2, 3)) // Int: 1
first(List("a", "b", "c")) // String: "a"
first(List(3.14, 2.72)) // Double: 3.14
Generic Type Parameters
You declare a type parameter inside square brackets [A]. The letter A is just a placeholder — you can name it anything, but single uppercase letters are conventional. Common names are A, B, T (for "Type"), K (for "Key"), and V (for "Value").
def identity[A](value: A): A = value
│
└── A is the type parameter.
The caller decides what A is.
identity(42) → A becomes Int
identity("hello") → A becomes String
identity(true) → A becomes Boolean
Generic Classes
Classes can also have type parameters:
class Box[A](val content: A):
def describe(): String = s"Box containing: $content"
def map[B](f: A => B): Box[B] = new Box(f(content))
val intBox = new Box(42)
val strBox = new Box("Scala")
val dblBox = intBox.map(_ * 2.5) // Box[Double]
println(intBox.describe()) // Box containing: 42
println(strBox.describe()) // Box containing: Scala
println(dblBox.describe()) // Box containing: 105.0
Multiple Type Parameters
A generic can have more than one type parameter:
class Pair[A, B](val first: A, val second: B):
def swap: Pair[B, A] = new Pair(second, first)
override def toString: String = s"($first, $second)"
val p1 = new Pair("Alice", 30)
println(p1) // (Alice, 30)
println(p1.swap) // (30, Alice)
val p2 = new Pair(true, 3.14)
println(p2) // (true, 3.14)
Generic Function with Multiple Type Parameters
def zip[A, B](as: List[A], bs: List[B]): List[(A, B)] =
as.zip(bs)
val names = List("Aarav", "Diya", "Ravi")
val scores = List(95, 87, 91)
val result = zip(names, scores)
result.foreach(println)
// (Aarav,95)
// (Diya,87)
// (Ravi,91)
Generic Data Structure: Stack
Building a Stack from scratch illustrates how generics power reusable data structures:
class Stack[A]:
private var elements: List[A] = List()
def push(item: A): Unit =
elements = item :: elements
def pop(): Option[A] =
elements match
case head :: tail =>
elements = tail
Some(head)
case Nil => None
def peek: Option[A] = elements.headOption
def isEmpty: Boolean = elements.isEmpty
def size: Int = elements.length
val intStack = new Stack[Int]
intStack.push(10)
intStack.push(20)
intStack.push(30)
println(intStack.pop()) // Some(30)
println(intStack.peek) // Some(20)
println(intStack.size) // 2
val strStack = new Stack[String]
strStack.push("first")
strStack.push("second")
println(strStack.pop()) // Some(second)
Stack[Int] Stack[String]
────────── ────────────
push(30) → [30] push("first") → ["first"]
push(20) → [20,30] push("second") → ["second","first"]
pop() → Some(30) pop() → Some("second")
peek → Some(20)
Type Bounds
Sometimes you want your generic type to have certain capabilities. Type bounds restrict which types a type parameter can accept.
Upper Bound: A must be a subtype of B
class Animal(val name: String)
class Dog(name: String) extends Animal(name)
class Cat(name: String) extends Animal(name)
def printNames[A <: Animal](animals: List[A]): Unit =
animals.foreach(a => println(a.name))
printNames(List(new Dog("Rex"), new Dog("Buddy"))) // works
printNames(List(new Cat("Luna"), new Cat("Milo"))) // works
// printNames(List(1, 2, 3)) // Error: Int is not a subtype of Animal
[A <: Animal] means "A must be Animal or a subclass of Animal"
│
└── <: means "is a subtype of"
Lower Bound: A must be a supertype of B
def fillList[A >: Dog](item: A, count: Int): List[A] =
List.fill(count)(item)
val dogs: List[Animal] = fillList(new Dog("Rex"), 3)
// List[Animal] because Animal is a supertype of Dog
Context Bounds (Type Class Pattern)
A context bound says "this type must have an implicit instance of a certain type class available." This is how you write generic functions that require certain behavior from their type parameter:
def maxOf[A: Ordering](a: A, b: A): A =
if summon[Ordering[A]].compare(a, b) >= 0 then a else b
println(maxOf(3, 7)) // 7
println(maxOf("apple", "mango")) // mango (lexicographic order)
The [A: Ordering] syntax means "A must have an Ordering available." Scala provides Ordering instances for all standard types automatically.
Invariance, Covariance, and Contravariance Preview
class Box[A] // Invariant: Box[Dog] is NOT a Box[Animal]
class Box[+A] // Covariant: Box[Dog] IS a Box[Animal]
class Box[-A] // Contravariant: Box[Animal] IS a Box[Dog]
This concept (variance) is covered in detail in the next topic. For now, know that +A (covariant) is what you see on List[+A] — a List[Dog] can be used anywhere a List[Animal] is expected.
Generic Option Implementation
Scala's built-in Option type is itself generic. Here is a simplified version to show how it works:
sealed trait MyOption[+A]
case class MySome[A](value: A) extends MyOption[A]
case object MyNone extends MyOption[Nothing]
def divide(a: Int, b: Int): MyOption[Int] =
if b == 0 then MyNone else MySome(a / b)
divide(10, 2) match
case MySome(result) => println(s"Result: $result")
case MyNone => println("Cannot divide by zero")
Reusable Generic Utilities
// Generic safe head (avoids exceptions)
def safeHead[A](list: List[A]): Option[A] =
if list.isEmpty then None else Some(list.head)
// Generic transform and filter
def transformAndFilter[A, B](items: List[A], f: A => Option[B]): List[B] =
items.flatMap(f)
val input = List("1", "two", "3", "four", "5")
val numbers = transformAndFilter(input, s =>
try Some(s.toInt) catch case _: NumberFormatException => None
)
println(numbers) // List(1, 3, 5)
When to Use Generics
Use generics when you write a function or class that works with values regardless of their type — like containers, utilities, or algorithms. Avoid making every class generic. If a class specifically manages users, let it work with users directly. Generics shine when the type truly does not matter to the logic — only the structure does.
