08 · Pattern Matching Intro¶
Basic match¶
You've already seen the simplest form of match in
Module 3 — matching literal values:
def dayName(day: Int): String =
day match
case 1 => "Monday"
case 2 => "Tuesday"
case 3 => "Wednesday"
case 4 => "Thursday"
case 5 => "Friday"
case 6 | 7 => "Weekend" // multiple patterns, same result
case _ => "Invalid day"
println(dayName(6)) // Weekend
println(dayName(9)) // Invalid day
match is exhaustiveness-friendly and generally preferred over long
if/else if chains once you have more than two or three branches.
Matching on type¶
def describe(x: Any): String =
x match
case i: Int => s"an Int: $i"
case s: String => s"a String of length ${s.length}"
case b: Boolean => s"a Boolean: $b"
case _ => "something else"
println(describe(42)) // an Int: 42
println(describe("hello")) // a String of length 5
println(describe(true)) // a Boolean: true
println(describe(3.14)) // something else
Destructuring case classes¶
This is where match really shines — you can match on a case class's
shape and bind its fields to names in one step:
case class Point(x: Int, y: Int)
def classify(p: Point): String =
p match
case Point(0, 0) => "origin"
case Point(0, _) => "on the y-axis"
case Point(_, 0) => "on the x-axis"
case Point(x, y) if x == y => "on the diagonal"
case Point(x, y) => s"a regular point at ($x, $y)"
println(classify(Point(0, 0))) // origin
println(classify(Point(0, 5))) // on the y-axis
println(classify(Point(5, 0))) // on the x-axis
println(classify(Point(3, 3))) // on the diagonal
println(classify(Point(2, 7))) // a regular point at (2, 7)
The if clauses above (case Point(x, y) if x == y => ...) are called
guards — an extra condition checked only after the shape matches.
Matching on lists¶
def describeList(xs: List[Int]): String =
xs match
case Nil => "empty list"
case x :: Nil => s"single element: $x"
case x :: y :: Nil => s"two elements: $x and $y"
case first :: rest => s"starts with $first, ${rest.length} more after"
println(describeList(Nil)) // empty list
println(describeList(List(5))) // single element: 5
println(describeList(List(1, 2))) // two elements: 1 and 2
println(describeList(List(1, 2, 3, 4))) // starts with 1, 3 more after
:: (pronounced "cons") is the same operator you used to prepend an element
to a list in Module 5 — here it's used in
reverse, to deconstruct a list into its head and tail.
Matching on tuples¶
def quadrant(point: (Int, Int)): String =
point match
case (0, 0) => "origin"
case (x, y) if x > 0 && y > 0 => "quadrant I"
case (x, y) if x < 0 && y > 0 => "quadrant II"
case (x, y) if x < 0 && y < 0 => "quadrant III"
case (x, y) if x > 0 && y < 0 => "quadrant IV"
case _ => "on an axis"
println(quadrant((3, 4))) // quadrant I
println(quadrant((-2, 5))) // quadrant II
println(quadrant((0, 7))) // on an axis
match as an expression¶
Like if/else, match produces a value — you rarely need a var to
accumulate a result across branches:
val n = 7
val parity = n match
case x if x % 2 == 0 => "even"
case _ => "odd"
println(parity) // odd
How It Actually Works¶
A match expression doesn't compile to one uniform mechanism — the
compiler picks the cheapest applicable strategy per pattern shape.
Matching on simple literal Ints or Strings (dense integer cases
especially) compiles to the JVM's tableswitch or lookupswitch
bytecode instruction — the same O(1) jump-table dispatch a Java switch
compiles to, rather than a chain of if/else if comparisons. Matching
on type (case s: String => ...) compiles to a sequence of
instanceof checks (JVM instanceof bytecode) followed by a cast,
evaluated top to bottom — which is exactly why pattern order matters and
why an unreachable/overly-general earlier case can shadow a later one.
Destructuring a case class (case Point(x, y) => ...) doesn't get special
runtime support either — it compiles to a call to the case class's
compiler-generated unapply method (see Module 7),
which returns Some((x, y)) on a structural match; the compiler then
extracts the tuple's fields into the bound names x and y. For a
non-case class, you get the same pattern-matching syntax only if you
hand-write a matching unapply in its companion object — the language
feature is really "call unapply and destructure what it returns," not
anything intrinsically about case classes.
Every match also gets a scala.MatchError thrown if no case applies and
there's no wildcard _ — the compiler inserts this fallback automatically,
which is why exhaustiveness checking on sealed hierarchies (covered in
Module 9 and Level 2's advanced pattern
matching) matters: it lets the
compiler prove at compile time that this runtime fallback can never
actually trigger.
Cheat sheet¶
| Pattern | Matches |
|---|---|
case 3 => |
The literal value 3 |
case 3 \| 4 => |
Either 3 or 4 |
case i: Int => |
Any value of type Int, bound to i |
case Point(x, y) => |
A Point, binding its fields to x and y |
case x :: rest => |
A non-empty list, binding head and tail |
case (a, b) => |
A 2-tuple, binding both elements |
case x if cond => |
Adds a guard condition after the shape matches |
case _ => |
Matches anything (catch-all) |
🔀 See this in another language¶
Exercise¶
Define case class Shape2(kind: String, a: Double, b: Double = 0.0) used to
represent either a "circle" (radius in a) or a "rectangle" (width a,
height b). Write a function area(shape: Shape2): Double that pattern
matches on shape.kind to compute the correct area, throwing a
MatchError-friendly message via case _ => throw new IllegalArgumentException(...)
for unknown kinds. Test it with a few circles and rectangles.