Scala - Match Expression (Pattern Matching)
• Pattern matching in Scala is a powerful control structure that combines type checking, destructuring, and conditional logic in a single expression, returning values unlike traditional switch...
Key Insights
• Pattern matching in Scala is a powerful control structure that combines type checking, destructuring, and conditional logic in a single expression, returning values unlike traditional switch statements • Match expressions support sophisticated patterns including type matching, case class decomposition, guards, and variable binding, enabling concise and type-safe code • Sealed traits combined with pattern matching provide compile-time exhaustiveness checking, preventing runtime errors from unhandled cases
Basic Match Expression Syntax
Scala’s match expression evaluates a value against a series of patterns, executing the first matching case and returning its result. Unlike Java’s switch statement, match is an expression that always returns a value.
def describeNumber(n: Int): String = n match {
case 0 => "zero"
case 1 => "one"
case 2 => "two"
case _ => "many"
}
println(describeNumber(0)) // "zero"
println(describeNumber(5)) // "many"
The underscore _ acts as a wildcard pattern, matching any value. Without a default case, a MatchError is thrown at runtime if no pattern matches.
def unsafeMatch(n: Int): String = n match {
case 1 => "one"
case 2 => "two"
// Missing default case - throws MatchError for other values
}
Type Pattern Matching
Match expressions can discriminate between types, providing type-safe downcasting without explicit isInstanceOf checks.
def processValue(value: Any): String = value match {
case s: String => s"String of length ${s.length}"
case i: Int => s"Integer: ${i * 2}"
case d: Double => f"Double: $d%.2f"
case list: List[_] => s"List with ${list.size} elements"
case _ => "Unknown type"
}
println(processValue("hello")) // "String of length 5"
println(processValue(42)) // "Integer: 84"
println(processValue(List(1, 2, 3))) // "List with 3 elements"
The matched value is automatically cast to the specified type within the case block, eliminating the need for manual casting.
Case Class Decomposition
Pattern matching excels at deconstructing case classes, extracting their components in a single operation.
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
case class Triangle(base: Double, height: Double) extends Shape
def calculateArea(shape: Shape): Double = shape match {
case Circle(r) => Math.PI * r * r
case Rectangle(w, h) => w * h
case Triangle(b, h) => 0.5 * b * h
}
val circle = Circle(5.0)
val rect = Rectangle(4.0, 6.0)
println(calculateArea(circle)) // 78.53981633974483
println(calculateArea(rect)) // 24.0
The compiler ensures exhaustiveness when matching on sealed traits, warning if any case is missing.
Guards and Conditional Patterns
Guards add conditional logic to patterns using if clauses, enabling more precise matching criteria.
def categorizeNumber(n: Int): String = n match {
case x if x < 0 => "negative"
case x if x == 0 => "zero"
case x if x > 0 && x <= 10 => "small positive"
case x if x > 10 && x <= 100 => "medium positive"
case _ => "large positive"
}
def processUser(name: String, age: Int): String = (name, age) match {
case (n, a) if n.isEmpty => "Name required"
case (n, a) if a < 0 => "Invalid age"
case (n, a) if a < 18 => s"$n is a minor"
case (n, a) if a >= 18 && a < 65 => s"$n is an adult"
case (n, a) => s"$n is a senior"
}
println(processUser("Alice", 25)) // "Alice is an adult"
println(processUser("", 30)) // "Name required"
Nested Pattern Matching
Patterns can be nested to match complex data structures in a single expression.
case class Address(street: String, city: String, country: String)
case class Person(name: String, age: Int, address: Address)
def describeLocation(person: Person): String = person match {
case Person(_, _, Address(_, _, "USA")) => "Lives in USA"
case Person(_, _, Address(_, "London", _)) => "Lives in London"
case Person(name, age, Address(street, city, country)) if age > 65 =>
s"$name is a senior living at $street, $city, $country"
case Person(name, _, Address(_, city, _)) =>
s"$name lives in $city"
}
val person1 = Person("Bob", 30, Address("Main St", "New York", "USA"))
val person2 = Person("Alice", 70, Address("Oak Ave", "Paris", "France"))
println(describeLocation(person1)) // "Lives in USA"
println(describeLocation(person2)) // "Alice is a senior living at Oak Ave, Paris, France"
Collection Pattern Matching
Scala provides specialized patterns for matching collection structures, particularly useful for recursive algorithms.
def sumList(numbers: List[Int]): Int = numbers match {
case Nil => 0
case head :: tail => head + sumList(tail)
}
def describeList[A](list: List[A]): String = list match {
case Nil => "empty list"
case head :: Nil => s"single element: $head"
case head :: tail => s"head: $head, tail has ${tail.length} elements"
}
println(sumList(List(1, 2, 3, 4))) // 10
println(describeList(List(1, 2, 3))) // "head: 1, tail has 2 elements"
println(describeList(List("only"))) // "single element: only"
You can match specific patterns within collections:
def processSequence(seq: List[Int]): String = seq match {
case List(1, 2, 3) => "exactly 1, 2, 3"
case List(1, _*) => "starts with 1"
case List(_, _, third, _*) => s"third element is $third"
case _ => "other sequence"
}
println(processSequence(List(1, 2, 3))) // "exactly 1, 2, 3"
println(processSequence(List(1, 5, 6))) // "starts with 1"
println(processSequence(List(9, 8, 7, 6))) // "third element is 7"
Variable Binding with @ Symbol
The @ operator binds the entire matched value to a variable while still matching its structure.
def processShape(shape: Shape): String = shape match {
case c @ Circle(r) if r > 10 =>
s"Large circle: $c with area ${calculateArea(c)}"
case r @ Rectangle(w, h) if w == h =>
s"Square: $r with area ${calculateArea(r)}"
case s =>
s"Regular shape with area ${calculateArea(s)}"
}
println(processShape(Circle(15.0)))
// "Large circle: Circle(15.0) with area 706.8583470577034"
println(processShape(Rectangle(5.0, 5.0)))
// "Square: Rectangle(5.0,5.0) with area 25.0"
Option Pattern Matching
Pattern matching is idiomatic for handling Option types, providing cleaner code than isDefined checks.
def getUserName(userId: Int): Option[String] = {
val users = Map(1 -> "Alice", 2 -> "Bob", 3 -> "Charlie")
users.get(userId)
}
def greetUser(userId: Int): String = getUserName(userId) match {
case Some(name) => s"Hello, $name!"
case None => "User not found"
}
println(greetUser(1)) // "Hello, Alice!"
println(greetUser(99)) // "User not found"
// Nested options
def processNestedOption(opt: Option[Option[Int]]): String = opt match {
case Some(Some(value)) => s"Value: $value"
case Some(None) => "Inner None"
case None => "Outer None"
}
Pattern Matching in Partial Functions
Partial functions use pattern matching to define functions for a subset of possible inputs.
val divide: PartialFunction[(Int, Int), Int] = {
case (numerator, denominator) if denominator != 0 =>
numerator / denominator
}
println(divide.isDefinedAt((10, 2))) // true
println(divide.isDefinedAt((10, 0))) // false
println(divide((10, 2))) // 5
val evenDoubler: PartialFunction[Int, Int] = {
case x if x % 2 == 0 => x * 2
}
val numbers = List(1, 2, 3, 4, 5)
println(numbers.collect(evenDoubler)) // List(4, 8)
Pattern matching transforms complex conditional logic into declarative, type-safe code. The combination of exhaustiveness checking, type inference, and destructuring capabilities makes it a cornerstone of idiomatic Scala programming. Use sealed traits for closed type hierarchies to leverage compile-time verification, ensuring all cases are handled without runtime surprises.