Scala Case Classes and Pattern Matching Complete Guide
Scala Case Classes and Pattern Matching Complete Guide
Case classes and pattern matching are among Scala's most celebrated features. Together they make domain modeling concise and exhaustive pattern matching safe. This module covers both in depth.
Case Classes: The Basics
A case class is a regular class with a set of auto-generated features:
case class Person(name: String, age: Int)
val alice = Person("Alice", 30)
val bob = Person("Bob", 25)
println(alice.name) // Alice
println(alice) // Person(Alice,30) — toString is auto-generated
println(alice == bob) // false
println(alice == Person("Alice", 30)) // true — equals compares fieldsCase classes automatically get:
toString— readable representationequalsandhashCode— structural equality based on fieldscopy— create modified copiesapply— constructor withoutnewunapply— enables pattern matching
The copy Method
val alice = Person("Alice", 30)
val olderAlice = alice.copy(age = 31)
println(olderAlice) // Person(Alice,31)copy creates a new instance with the same field values, allowing you to override specific fields. Case classes are immutable by default — you can't modify them, you create new ones.
Pattern Matching with match
Pattern matching is Scala's powerful alternative to switch:
def describe(x: Any): String = x match {
case 0 => "zero"
case 1 => "one"
case n: Int => s"other int: $n"
case s: String => s"string: $s"
case _ => "something else"
}
println(describe(0)) // zero
println(describe(42)) // other int: 42
println(describe("hi")) // string: hiThe _ wildcard matches anything and is the default case.
Destructuring Case Classes
Pattern matching can destructure case class instances:
case class Point(x: Double, y: Double)
case class Line(start: Point, end: Point)
val line = Line(Point(0, 0), Point(3, 4))
line match {
case Line(Point(0, 0), end) =>
println(s"Line from origin to $end")
case Line(start, end) =>
println(s"Line from $start to $end")
}
// Line from origin to Point(3.0,4.0)Destructuring works recursively — you can match nested structures in one expression.
Guard Clauses
Add conditions to patterns with if:
def classify(n: Int): String = n match {
case x if x < 0 => "negative"
case 0 => "zero"
case x if x % 2 == 0 => s"positive even: $x"
case x => s"positive odd: $x"
}
println(classify(-5)) // negative
println(classify(0)) // zero
println(classify(4)) // positive even: 4
println(classify(7)) // positive odd: 7Sealed Traits and ADTs
Sealed traits combined with case classes create algebraic data types (ADTs) — a fundamental functional programming pattern:
sealed trait Result[+A]
case class Success[A](value: A) extends Result[A]
case class Failure(error: String) extends Result[Nothing]
def divide(a: Int, b: Int): Result[Double] =
if (b == 0) Failure("Division by zero")
else Success(a.toDouble / b)
val result = divide(10, 2)
result match {
case Success(value) => println(s"Result: $value")
case Failure(error) => println(s"Error: $error")
}
// Result: 5.0The sealed keyword means all subtypes must be in the same file. The compiler can then verify that pattern matching is exhaustive — it warns if you miss a case.
Matching Collections
Pattern matching works on lists and other collections:
def sumList(list: List[Int]): Int = list match {
case Nil => 0
case head :: tail => head + sumList(tail)
}
println(sumList(List(1, 2, 3, 4, 5))) // 15Nil matches an empty list. head :: tail deconstructs a list into its first element and the rest.
The unapply Extractor Pattern
Custom extractors implement unapply to enable pattern matching on any type:
object PositiveInt {
def unapply(n: Int): Option[Int] =
if (n > 0) Some(n) else None
}
object EvenInt {
def unapply(n: Int): Option[Int] =
if (n % 2 == 0) Some(n) else None
}
def describe(n: Int): String = n match {
case PositiveInt(x) if EvenInt.unapply(x).isDefined => s"positive even: $x"
case PositiveInt(x) => s"positive: $x"
case 0 => "zero"
case x => s"negative: $x"
}Matching Tuples
Tuples can be pattern matched directly:
val coords = (3, -4)
coords match {
case (0, 0) => println("Origin")
case (x, 0) => println(s"On x-axis at $x")
case (0, y) => println(s"On y-axis at $y")
case (x, y) => println(s"Point at ($x, $y)")
}
// Point at (3, -4)Frequently Asked Questions
Q: Why are case classes immutable by default?
Immutability makes code easier to reason about — a case class value is just data. It also makes case classes safe to use as Map keys and in Sets (since they can't change, their hashCode won't change either). When you need to "modify" a case class, you use copy to create a new instance, which is the functional programming approach.
Q: What happens if I forget a case in a sealed trait match?
If you use a sealed trait and miss a case in your match expression, the Scala compiler gives a warning: "match may not be exhaustive". This is a compile-time safety net — it doesn't crash at runtime, but the missing case will throw a MatchError. Using sealed traits and enabling -Xfatal-warnings in SBT turns these into hard compile errors, which is a good practice.
Q: Can case classes extend other case classes?
No — Scala does not allow case-to-case inheritance. A case class cannot extend another case class. The reason is that the auto-generated equals and hashCode methods would be inconsistent with inheritance. Instead, use a sealed trait as the base and make each variant a separate case class.
Part of Scala Mastery Course — Module 8 of 22.
