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.

Leave a Comment

Your email address will not be published. Required fields are marked *