पाठ 10 / 25

Pattern Matching

Destructure data with match, guards, extractors and typed patterns.

match is a powerful expression

match compares a value against patterns and returns the result of the first matching case. Patterns include literals (case 0 =>), variables (case n =>), the wildcard _, typed patterns (case s: String =>), constructor patterns for case classes and enums (case Item(sku, qty) =>), tuples (case (a, b) =>), lists (case head :: tail =>, case Nil =>), alternatives (case "y" | "yes" =>), binders (case p @ Payment.Card(_) =>) and guards (case Item(_, q) if q > 10 =>). A match that finds nothing throws MatchError, and the compiler warns about non-exhaustive matches on sealed types. Extractor objects define unapply to make custom patterns, for example parsing a PIN code from a string. Pattern matching also appears in val (a, b) = pair, in for-comprehension generators and in partial functions ({ case ... } blocks) passed to collect. Prefer matching on ADTs over isInstanceOf checks and casts.

Pattern matching as a sorter

Each case checks shape and conditions, binding the parts it needs.

A value travelling down a column of stencils of different shapes, stopping at the first one it fits, with extracted parts shown beside it.
Figure 4.1 — match tries cases in order.

Patterns on lists, tuples, case classes and extractors

Guards, binders and a custom extractor.

case class Item(sku: String, qty: Int, pricePaise: Long)

def summarise(items: List[Item]): String = items match
  case Nil                       => "empty cart"
  case Item(sku, 1, _) :: Nil    => s"just one $sku"
  case first :: rest if rest.size > 5 => s"${first.sku} and ${rest.size} more (bulk order)"
  case first :: rest             => s"${first.sku} and ${rest.size} more"

object PinCode:
  def unapply(s: String): Option[Int] =
    if s.matches("""\d{6}""") then Some(s.toInt) else None

def classify(input: Any): String = input match
  case 0                           => "zero"
  case n: Int if n < 0             => "negative number"
  case PinCode(pin)                => s"PIN code $pin"
  case s: String if s.isBlank      => "blank text"
  case (city: String, count: Int)  => s"$count orders in $city"
  case item @ Item(_, qty, _) if qty > 100 => s"bulk item ${item.sku}"
  case _                           => "something else"

println(summarise(List(Item("pen", 1, 4950))))   // just one pen
println(classify("411001"))                       // PIN code 411001
println(classify(("Pune", 12)))                   // 12 orders in Pune

Order matters

Cases are tried top to bottom. Put specific cases (with guards) before general ones; a case _ or case first :: rest placed too early silently hides the cases after it, and the compiler warns that they are unreachable.

त्वरित जाँच: What happens when no case matches a value in a match expression?

  • It returns null
  • The first case is used
  • A MatchError is thrown at run time
  • The program fails to compile in every case
Answer

A MatchError is thrown at run time — Non-exhaustive matches can fail at run time with MatchError; sealed types let the compiler warn first.