This week I am taking a look at Pattern Matching in Scala Essentials | Rock the JVM.
Pattern Matching in Scala #
Basic Pattern Matching #
- Switch on steroids: Pattern matching is like switch but more powerful
- Case patterns: Match values with
caseexpressions, with_as default - Decomposition: Extract values from case classes and other structures
- Guards: Add conditional logic with
ifexpressions in case patterns - Sealed hierarchies: Pattern matching works well with sealed class hierarchies
- Exhaustive matching: Compiler warns about missing cases with sealed types
// Generate a random integer and match against specific values
val random = new Random()
val aValue = random.nextInt(100)
val description = aValue match {
case 1 => "the first"
case 2 => "the second"
case 3 => "the third"
// Wildcard pattern catches all other values
// Without it, MatchError would be thrown for non-matching values
case _ => s"something else: $aValue"
} Basic pattern matching with integer values, demonstrating how to match specific values and provide a default case using the wildcard pattern (_).
// Case classes provide built-in extractor patterns for pattern matching
case class Person(name: String, age: Int)
val bob = Person("Bob", 16)
val greeting = bob match {
// Guard condition restricts when this pattern applies
case Person(n, a) if a < 18 =>
s"Hi, my name is $n and I'm $a years old."
// Pattern without guard, extracts name and age
case Person(n, a) =>
s"Hello there, my name is $n and I'm not allowed to say my age."
// Fallback case (unreachable here, but good practice)
case _ =>
"I don't know who I am."
} Decomposing case class instances using pattern matching, with guard conditions to create different behaviors based on extracted values.
// Sealed hierarchies work well with pattern matching
// - Compiler can check for exhaustiveness
// - Patterns are matched in order (most specific first)
// - Return type is the lowest common ancestor of all branches
sealed class Animal
case class Dog(breed: String) extends Animal
case class Cat(meowStyle: String) extends Animal
val anAnimal: Animal = Dog("Terra Nova")
val animalPM = anAnimal match {
case Dog(someBreed) => "I've detected a dog"
case Cat(meow) => "I've detected a cat"
// Type pattern catches any other Animal subtype
// (unreachable in this sealed hierarchy)
case _: Animal => ???
}Exercise: Expression Formatter #
This exercise demonstrates using pattern matching to convert abstract syntax tree (AST) expressions into human-readable mathematical notation, with proper operator precedence handling.
/**
* Exercise: Create a function that converts expressions to strings
* Requirements:
* show(Sum(Number(2), Number(3))) = "2 + 3"
* show(Sum(Sum(Number(2), Number(3)), Number(4)) = "2 + 3 + 4"
* show(Prod(Sum(Number(2), Number(3)), Number(4))) = "(2 + 3) * 4"
* show(Sum(Prod(Number(2), Number(3)), Number(4)) = "2 * 3 + 4"
*/
sealed trait Expr
case class Number(n: Int) extends Expr
case class Sum(e1: Expr, e2: Expr) extends Expr
case class Prod(e1: Expr, e2: Expr) extends Expr
def show(expr: Expr): String = expr match {
// Base case: convert number to string
case Number(n) => s"$n"
// Sum: recursively convert both operands
case Sum(left, right) => show(left) + " + " + show(right)
// Product: handle parentheses for operator precedence
case Prod(left, right) => {
// Helper function to add parentheses only around Sum expressions
def maybeShowParentheses(exp: Expr) = exp match {
case Prod(_, _) => show(exp)
// No parentheses needed for nested Prod
case Number(_) => show(exp)
// No parentheses needed for Number
case Sum(_, _) => s"(${show(exp)})"
// Parentheses needed for Sum
}
maybeShowParentheses(left) + " * " + maybeShowParentheses(right)
}
}Advanced Pattern Matching Techniques #
Various pattern matching techniques beyond the basics, showing the versatility and power of Scala’s pattern matching.
Matching Constants and Values #
object MySingleton
// Pattern matching against various constant types
val someValue: Any = "Scala"
val constants = someValue match {
case 42 => "a number"
case "Scala" => "THE Scala"
case true => "the truth"
case MySingleton => "a singleton object"
} Wildcards and Variable Binding #
// Variable binding: captures the matched value in a variable
val matchAnythingVar = 2 + 3 match {
// Variable 'something' will contain the value 5
case something => s"I've matched anything, it's $something"
}
// Wildcard pattern: matches anything but doesn't bind to a variable
val matchAnything = someValue match {
// Underscore discards the matched value
case _ => "I can match anything at all"
} Matching Tuples and Nested Structures #
// Matching tuple elements with fixed values and variables
val aTuple = (1,4)
val matchTuple = aTuple match {
// Match first element exactly, bind second to variable
case (1, somethingElse) =>
s"A tuple with 1 and $somethingElse"
// Match second element exactly, bind first to variable
case (something, 2) =>
"A tuple with 2 as its second field"
}
// Pattern matching works with nested data structures
val nestedTuple = (1, (2, 3))
val matchNestedTuple = nestedTuple match {
case (_, (2, v)) => "A nested tuple ..."
} Matching Case Classes and Collections #
// Pattern matching with custom linked list implementation
val aList: LList[Int] = Cons(1, Cons(2, Empty()))
val matchList = aList match {
case Empty() => "an empty list"
case Cons(head, Cons(_, tail)) =>
s"a non-empty list starting with $head"
}
// Pattern matching with Option type
val anOption: Option[Int] = Option(2)
val matchOption = anOption match {
case None => "an empty option"
case Some(value) => s"non-empty, got $value"
} List Pattern Matching #
// Standard library List patterns
val aStandardList = List(1,2,3,42)
val matchStandardList = aStandardList match {
case List(1, _, _, _) =>
"list with 4 elements, first is 1"
// Variable-length pattern (_*)
// List must start with 1, can have any number of elements after
case List(1, _*) =>
"list starting with 1"
// Using :+ for matching the last element
// List must start with 1,2,_, and end with 42
case List(1, 2, _) :+ 42 =>
"list ending in 42"
// Cons operator (::) splits list into head and tail
case head :: tail =>
"deconstructed list"
} Type Pattern Matching #
// Match against runtime type information
// Useful when working with Any or supertype references
val unknown: Any = 2
val matchTyped = unknown match {
case anInt: Int =>
s"I matched an int, I can add 2 to it: ${anInt + 2}"
case aString: String =>
"I matched a String"
case _: Double =>
"I matched a double I don't care about"
} Pattern Binding and Guards #
// @ operator binds a name to a whole pattern
val bindingNames = aList match {
// Bind 'rest' to the entire Cons(_, tail) structure
// while also matching its components
case Cons(head, rest @ Cons(_, tail)) =>
s"Can use $rest" // rest refers to the whole Cons(_, tail)
}
// OR patterns using | operator
val multiMatch = aList match {
// Match either Empty() OR a Cons with 0 as head
case Empty() | Cons(0, _) =>
"an empty list to me"
case _ =>
"anything else"
}
// Guard conditions add extra constraints to patterns
val secondElementSpecial = aList match {
case Cons(_, Cons(specialElement, _)) if specialElement > 5 =>
"second element is big enough"
case _ =>
"anything else"
}Type Erasure Gotchas #
/**
* Exercise (trick question):
* What will the following code return?
*/
val numbers: List[Int] = List(1,2,3,4)
val numbersMatch = numbers match {
case listOfStrings: List[String] => "a list of strings"
case listOfInts: List[Int] => "a list of numbers"
}
// ANSWER: Returns "a list of strings" despite being List[Int]!
//
// Due to JVM type erasure:
// - Generic type information is removed at runtime
// - Both patterns check only for raw List type
// - First case matches because pattern matcher only sees List
// - Second case is never reached
// Type erasure affects all generic types:
// List[String] → List
// List[Int] → List
// Function1[Int, String] → Function1
// Map[String, Int] → Map
// etc.This example illustrates a common pitfall with pattern matching and JVM type erasure. Since generic type information is erased at runtime, pattern matching on generic types can lead to unexpected results. In this case, the code returns “a list of strings” even though we’re matching against a List[Int], because at runtime the JVM only sees the raw List type, not its generic parameters.
Beyond Pattern Matching #
Pattern matching in Scala extends beyond explicit match expressions, appearing in various language constructs for powerful, concise code.
Key Applications #
- Error handling: Pattern matching is used in
try-catchblocks - For comprehensions: Use pattern matching for generators
- Destructuring syntax: Python-like syntax for tuples and collections
- Type erasure: Generic type information is erased at runtime
Pattern Matching in Try-Catch Blocks #
// Under the hood: try-catch blocks use pattern matching on exceptions
val potentialFailure = try {
// code
} catch {
case e: RuntimeException => "runtime ex"
case npe: NullPointerException => "npe"
case _ => "some other exception"
}
// Conceptually equivalent to:
// try {
// code
// } catch (e) {
// e match {
// case e: RuntimeException => "runtime ex"
// case npe: NullPointerException => "npe"
// case _ => "some other exception"
// }
// } Pattern Matching in For Comprehensions #
// For comprehensions use pattern matching in generators
val aList = List(1,2,3,4)
// Simple variable binding with filter
val evenNumbers = for {
// Bind each element to 'n' and filter with guard
n <- aList if n % 2 == 0
} yield 10 * n
// Tuple destructuring in for comprehensions
val tuples = List((1,2), (3,4))
val filterTuples = for {
// Destructure each tuple into 'first' and 'second' variables
// This is pattern matching!
(first, second) <- tuples if first < 3
} yield second * 100 For comprehensions use pattern matching in their generators, allowing direct destructuring of collection elements. The (first, second) <- tuples line is decomposing each tuple into components using pattern matching.
Pattern Matching in Variable Assignments #
// Destructuring assignment - pattern matching without 'match' keyword
val aTuple = (1,2,3)
// Extract all elements from tuple directly into variables
val (a, b, c) = aTuple
// Extract head and tail of a list using cons pattern
// Equivalent to: val head = tuples.head; val tail = tuples.tail
val head :: tail = tuples