Pattern Matching: Deconstructing Data

Introduction

Pattern matching is one of Scala's most powerful and distinctive features. It allows you to check a value against a pattern and extract data from complex structures in a single, elegant expression. Unlike simple switch statements in other languages, Scala's pattern matching can destructure objects, match on types, use guards, and even work with custom extractors.

This lesson will teach you to use pattern matching effectively, making your code more readable, maintainable, and expressive. You'll learn to think in patterns and leverage this powerful tool for control flow, data extraction, and validation.

Basic Pattern Matching

Match Expressions

The basic syntax uses match with case clauses:

// Basic pattern matching with literals
def describeNumber(x: Int): String = x match {
  case 0 => "zero"
  case 1 => "one"
  case 2 => "two"
  case _ => "other number"  // Default case (wildcard)
}

println(describeNumber(0))  // zero
println(describeNumber(1))  // one
println(describeNumber(5))  // other number

// Pattern matching with different types
def describeValue(value: Any): String = value match {
  case 42 => "The answer to everything"
  case "hello" => "A greeting"
  case true => "Boolean true"
  case x: Int => s"An integer: $x"
  case x: String => s"A string: $x"
  case _ => "Something else"
}

println(describeValue(42))      // The answer to everything
println(describeValue("hello")) // A greeting
println(describeValue(100))     // An integer: 100
println(describeValue("world")) // A string: world
println(describeValue(3.14))    // Something else

// Pattern matching with ranges and conditions
def categorizeAge(age: Int): String = age match {
  case x if x < 0 => "Invalid age"
  case 0 to 12 => "Child"
  case 13 to 19 => "Teenager"
  case 20 to 64 => "Adult"
  case x if x >= 65 => "Senior"
}

println(categorizeAge(8))   // Child
println(categorizeAge(16))  // Teenager
println(categorizeAge(35))  // Adult
println(categorizeAge(70))  // Senior

// Multiple patterns in one case
def isWeekend(day: String): Boolean = day.toLowerCase match {
  case "saturday" | "sunday" => true
  case _ => false
}

println(isWeekend("Saturday"))  // true
println(isWeekend("monday"))    // false

// Pattern matching with variable binding
def processNumber(x: Int): String = x match {
  case 0 => "zero"
  case n if n > 0 => s"positive: $n"
  case n => s"negative: $n"
}

println(processNumber(5))   // positive: 5
println(processNumber(-3))  // negative: -3

Guards and Conditions

Guards allow you to add boolean conditions to patterns:

// Guards with if conditions
def classifyNumber(x: Int): String = x match {
  case n if n < 0 => "negative"
  case 0 => "zero"
  case n if n % 2 == 0 => "positive even"
  case n if n % 2 == 1 => "positive odd"
}

println(classifyNumber(-5))  // negative
println(classifyNumber(0))   // zero
println(classifyNumber(4))   // positive even
println(classifyNumber(7))   // positive odd

// Complex guards
def evaluateGrade(score: Int, subject: String): String = (score, subject) match {
  case (s, "Math") if s >= 90 => "Excellent in Math!"
  case (s, "English") if s >= 85 => "Great English skills!"
  case (s, subj) if s >= 70 => s"Good performance in $subj"
  case (s, subj) if s >= 50 => s"Passing grade in $subj"
  case (s, subj) => s"Needs improvement in $subj (score: $s)"
}

println(evaluateGrade(95, "Math"))     // Excellent in Math!
println(evaluateGrade(88, "English"))  // Great English skills!
println(evaluateGrade(75, "Science"))  // Good performance in Science
println(evaluateGrade(45, "History"))  // Needs improvement in History (score: 45)

// Guards with complex conditions
case class Person(name: String, age: Int, isStudent: Boolean)

def getDiscount(person: Person): Double = person match {
  case Person(_, age, true) if age < 18 => 0.5   // Student under 18: 50% off
  case Person(_, age, true) => 0.2               // Student 18+: 20% off
  case Person(_, age, false) if age >= 65 => 0.3 // Senior citizen: 30% off
  case Person(_, age, false) if age < 12 => 0.4  // Child: 40% off
  case _ => 0.0                                  // No discount
}

val students = List(
  Person("Alice", 16, true),
  Person("Bob", 20, true),
  Person("Charlie", 70, false),
  Person("Diana", 8, false),
  Person("Eve", 35, false)
)

students.foreach { person =>
  val discount = getDiscount(person)
  println(f"${person.name}: ${discount * 100}%.0f%% discount")
}

// Pattern matching with mathematical operations
def mathOperation(operation: String, x: Double, y: Double): Either[String, Double] = 
  operation.toLowerCase match {
    case "add" | "+" => Right(x + y)
    case "subtract" | "-" => Right(x - y)
    case "multiply" | "*" => Right(x * y)
    case "divide" | "/" if y != 0 => Right(x / y)
    case "divide" | "/" => Left("Division by zero")
    case "power" | "^" => Right(math.pow(x, y))
    case op => Left(s"Unknown operation: $op")
  }

val operations = List(
  ("add", 5.0, 3.0),
  ("divide", 10.0, 2.0),
  ("divide", 10.0, 0.0),
  ("power", 2.0, 3.0),
  ("unknown", 1.0, 2.0)
)

operations.foreach { case (op, x, y) =>
  mathOperation(op, x, y) match {
    case Right(result) => println(f"$x $op $y = $result")
    case Left(error) => println(s"Error: $error")
  }
}

Pattern Matching with Collections

Lists and Sequences

// Basic list pattern matching
def listDescription(list: List[Int]): String = list match {
  case Nil => "Empty list"
  case List(x) => s"Single element: $x"
  case List(x, y) => s"Two elements: $x and $y"
  case x :: Nil => s"Single element using cons: $x"
  case x :: y :: Nil => s"Two elements using cons: $x and $y"
  case head :: tail => s"Head: $head, Tail: $tail"
}

println(listDescription(List()))           // Empty list
println(listDescription(List(42)))         // Single element: 42
println(listDescription(List(1, 2)))       // Two elements: 1 and 2
println(listDescription(List(1, 2, 3, 4))) // Head: 1, Tail: List(2, 3, 4)

// Advanced list pattern matching
def processIntList(list: List[Int]): String = list match {
  case Nil => "No numbers to process"
  case List(x) if x > 10 => s"Single large number: $x"
  case List(x) => s"Single small number: $x"
  case x :: y :: _ if x > y => s"Starts with decreasing: $x > $y"
  case x :: y :: _ if x < y => s"Starts with increasing: $x < $y"
  case x :: y :: _ => s"Starts with equal: $x = $y"
}

val testLists = List(
  List(),
  List(15),
  List(5),
  List(10, 5, 3),
  List(1, 8, 2),
  List(7, 7, 9)
)

testLists.foreach { list =>
  println(s"$list: ${processIntList(list)}")
}

// String list processing
def analyzeWords(words: List[String]): String = words match {
  case Nil => "No words"
  case List(word) => s"Single word: '$word' (${word.length} chars)"
  case first :: second :: rest if first.length > second.length =>
    s"Starts with longer word: '$first' > '$second'"
  case first :: second :: rest =>
    s"First two words: '$first' and '$second', plus ${rest.length} more"
}

val wordLists = List(
  List(),
  List("hello"),
  List("programming", "fun"),
  List("Scala", "is", "awesome", "for", "functional", "programming")
)

wordLists.foreach { words =>
  println(s"${words.mkString("[", ", ", "]")}: ${analyzeWords(words)}")
}

// Pattern matching with sequences and vectors
def sequencePattern[T](seq: Seq[T]): String = seq match {
  case Seq() => "Empty sequence"
  case Seq(single) => s"Single element sequence: $single"
  case Seq(first, second) => s"Two element sequence: $first, $second"
  case Seq(first, _*) => s"Sequence starting with: $first"
}

println(sequencePattern(Vector()))              // Empty sequence
println(sequencePattern(Vector(42)))            // Single element sequence: 42
println(sequencePattern(Array(1, 2)))           // Two element sequence: 1, 2
println(sequencePattern(List(1, 2, 3, 4, 5)))   // Sequence starting with: 1

// Extracting specific positions
def listAnalysis(numbers: List[Int]): String = numbers match {
  case List() => "Empty"
  case List(a) => s"[$a]"
  case List(a, b) => s"[$a, $b]"
  case a :: b :: c :: Nil => s"Exactly three: [$a, $b, $c]"
  case a :: b :: tail if tail.length > 5 => s"Long list: starts [$a, $b], ${tail.length} more"
  case a :: b :: tail => s"Medium list: starts [$a, $b], tail: $tail"
}

val numberLists = List(
  List(),
  List(1),
  List(1, 2),
  List(1, 2, 3),
  List(1, 2, 3, 4, 5),
  List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
)

numberLists.foreach { list =>
  println(s"${list.take(3).mkString("[", ", ", if (list.length > 3) ", ...]" else "]")}: ${listAnalysis(list)}")
}

Tuples and Case Classes

// Tuple pattern matching
def describePair(pair: (Int, String)): String = pair match {
  case (0, str) => s"Zero with string: $str"
  case (num, "") => s"Number $num with empty string"
  case (num, str) if num > 0 => s"Positive $num with '$str'"
  case (num, str) => s"Negative $num with '$str'"
}

val pairs = List(
  (0, "hello"),
  (42, ""),
  (10, "world"),
  (-5, "negative")
)

pairs.foreach { pair =>
  println(s"$pair: ${describePair(pair)}")
}

// Triple pattern matching
def analyzeTriple(triple: (String, Int, Boolean)): String = triple match {
  case ("admin", _, true) => "Admin user with permissions"
  case ("admin", _, false) => "Admin user without permissions"
  case (name, age, true) if age >= 18 => s"Adult user $name with permissions"
  case (name, age, true) => s"Minor user $name with permissions"
  case (name, age, false) => s"User $name (age $age) without permissions"
}

val userTriples = List(
  ("admin", 30, true),
  ("admin", 25, false),
  ("john", 25, true),
  ("alice", 16, true),
  ("bob", 35, false)
)

userTriples.foreach { triple =>
  println(s"$triple: ${analyzeTriple(triple)}")
}

// Case class pattern matching
case class Student(name: String, grade: Int, subjects: List[String])
case class Teacher(name: String, subject: String, experience: Int)
case class Administrator(name: String, department: String)

def describePerson(person: Any): String = person match {
  case Student(name, grade, subjects) if grade >= 90 =>
    s"Excellent student $name studying ${subjects.mkString(", ")}"
  case Student(name, grade, subjects) if grade >= 70 =>
    s"Good student $name in ${subjects.length} subjects"
  case Student(name, grade, _) =>
    s"Student $name needs improvement (grade: $grade)"
  case Teacher(name, subject, exp) if exp > 10 =>
    s"Experienced $subject teacher $name ($exp years)"
  case Teacher(name, subject, exp) =>
    s"$subject teacher $name ($exp years experience)"
  case Administrator(name, dept) =>
    s"Administrator $name in $dept department"
  case _ =>
    "Unknown person type"
}

val people = List(
  Student("Alice", 95, List("Math", "Physics")),
  Student("Bob", 75, List("English", "History", "Art")),
  Student("Charlie", 60, List("Math")),
  Teacher("Dr. Smith", "Mathematics", 15),
  Teacher("Ms. Johnson", "English", 5),
  Administrator("Mr. Brown", "Academic Affairs")
)

people.foreach { person =>
  println(describePerson(person))
}

// Nested case class matching
case class Address(street: String, city: String, country: String)
case class Employee(name: String, position: String, address: Address, salary: Double)

def analyzeEmployee(employee: Employee): String = employee match {
  case Employee(name, "CEO", _, salary) =>
    f"CEO $name with salary $$${salary}%.0f"
  case Employee(name, "Manager", Address(_, city, "USA"), salary) if salary > 80000 =>
    f"High-paid US Manager $name in $city"
  case Employee(name, position, Address(_, _, country), _) if country != "USA" =>
    s"International employee $name ($position) in $country"
  case Employee(name, position, Address(_, city, _), salary) =>
    f"Employee $name ($position) in $city, salary: $$${salary}%.0f"
}

val employees = List(
  Employee("John", "CEO", Address("123 Main St", "New York", "USA"), 250000),
  Employee("Sarah", "Manager", Address("456 Oak Ave", "Boston", "USA"), 95000),
  Employee("Pierre", "Developer", Address("789 Rue de la Paix", "Paris", "France"), 60000),
  Employee("Mike", "Analyst", Address("321 Pine St", "Seattle", "USA"), 70000)
)

employees.foreach { emp =>
  println(analyzeEmployee(emp))
}

Advanced Pattern Matching

Option and Either Patterns

// Option pattern matching
def processOption(opt: Option[String]): String = opt match {
  case Some(value) if value.nonEmpty => s"Got value: $value"
  case Some("") => "Got empty string"
  case None => "No value"
}

val options = List(Some("hello"), Some(""), None)
options.foreach { opt =>
  println(s"$opt: ${processOption(opt)}")
}

// Nested Option matching
def processNestedOption(opt: Option[Option[Int]]): String = opt match {
  case Some(Some(n)) if n > 0 => s"Positive nested value: $n"
  case Some(Some(n)) => s"Non-positive nested value: $n"
  case Some(None) => "Outer Some with inner None"
  case None => "Outer None"
}

val nestedOptions = List(
  Some(Some(42)),
  Some(Some(-5)),
  Some(None),
  None
)

nestedOptions.foreach { opt =>
  println(s"$opt: ${processNestedOption(opt)}")
}

// Either pattern matching
def processResult(result: Either[String, Int]): String = result match {
  case Right(value) if value > 100 => s"Large success value: $value"
  case Right(value) => s"Success value: $value"
  case Left(error) if error.contains("timeout") => s"Timeout error: $error"
  case Left(error) => s"Error: $error"
}

val results = List(
  Right(150),
  Right(42),
  Left("Connection timeout occurred"),
  Left("Invalid input")
)

results.foreach { result =>
  println(processResult(result))
}

// Complex Either chains
case class User(id: Int, name: String, email: String)

def validateUser(id: Int, name: String, email: String): Either[String, User] = {
  (id, name, email) match {
    case (i, _, _) if i <= 0 => Left("Invalid ID: must be positive")
    case (_, n, _) if n.trim.isEmpty => Left("Invalid name: cannot be empty")
    case (_, _, e) if !e.contains("@") => Left("Invalid email: must contain @")
    case (validId, validName, validEmail) => Right(User(validId, validName.trim, validEmail.toLowerCase))
  }
}

val userInputs = List(
  (1, "John Doe", "john@example.com"),
  (0, "Jane", "jane@test.com"),
  (-1, "", "invalid"),
  (2, "   ", "test@example.com"),
  (3, "Bob", "invalid-email")
)

userInputs.foreach { case (id, name, email) =>
  validateUser(id, name, email) match {
    case Right(user) => println(s"✓ Valid user: $user")
    case Left(error) => println(s"✗ $error")
  }
}

Custom Extractors

// Custom extractor using unapply
object Even {
  def unapply(n: Int): Option[Int] = {
    if (n % 2 == 0) Some(n) else None
  }
}

object Odd {
  def unapply(n: Int): Option[Int] = {
    if (n % 2 != 0) Some(n) else None
  }
}

def numberType(n: Int): String = n match {
  case Even(x) => s"$x is even"
  case Odd(x) => s"$x is odd"
}

(1 to 10).foreach { n =>
  println(numberType(n))
}

// Boolean extractor
object Positive {
  def unapply(n: Int): Boolean = n > 0
}

object Negative {
  def unapply(n: Int): Boolean = n < 0
}

def signDescription(n: Int): String = n match {
  case 0 => "zero"
  case Positive() => "positive"
  case Negative() => "negative"
}

List(-5, 0, 3, -2, 7).foreach { n =>
  println(s"$n is ${signDescription(n)}")
}

// Multi-value extractor
object Person {
  def unapply(person: String): Option[(String, String)] = {
    val parts = person.split(",").map(_.trim)
    if (parts.length == 2) Some((parts(0), parts(1)))
    else None
  }
}

object FullName {
  def unapply(name: String): Option[(String, String)] = {
    val parts = name.split(" ").map(_.trim)
    if (parts.length >= 2) Some((parts(0), parts.drop(1).mkString(" ")))
    else None
  }
}

def parsePerson(input: String): String = input match {
  case Person(FullName(first, last), age) => 
    s"Person: $first $last, age $age"
  case Person(name, age) => 
    s"Person: $name, age $age"
  case _ => 
    "Invalid person format"
}

val personStrings = List(
  "John Doe, 30",
  "Jane Smith Wilson, 25",
  "Bob, 40",
  "Invalid format"
)

personStrings.foreach { input =>
  println(s"'$input' -> ${parsePerson(input)}")
}

// Email extractor
object Email {
  def unapply(email: String): Option[(String, String)] = {
    val parts = email.split("@")
    if (parts.length == 2 && parts(0).nonEmpty && parts(1).nonEmpty) {
      Some((parts(0), parts(1)))
    } else None
  }
}

object Domain {
  def unapply(domain: String): Option[String] = {
    domain.split("\\.").lastOption.filter(_.nonEmpty)
  }
}

def analyzeEmail(email: String): String = email match {
  case Email(user, Domain("com")) => s"Commercial email: $user@[...].com"
  case Email(user, Domain("edu")) => s"Educational email: $user@[...].edu"
  case Email(user, Domain("org")) => s"Organization email: $user@[...].org"
  case Email(user, domain) => s"Email: $user@$domain"
  case _ => "Invalid email format"
}

val emails = List(
  "john@example.com",
  "student@university.edu",
  "contact@nonprofit.org",
  "user@custom.domain",
  "invalid-email"
)

emails.foreach { email =>
  println(s"$email -> ${analyzeEmail(email)}")
}

// Regex extractor
import scala.util.matching.Regex

val PhoneNumber: Regex = """(\d{3})-(\d{3})-(\d{4})""".r
val InternationalPhone: Regex = """\+(\d{1,3})\s(\d{3})-(\d{3})-(\d{4})""".r

def formatPhone(phone: String): String = phone match {
  case PhoneNumber(area, exchange, number) => 
    s"US Phone: ($area) $exchange-$number"
  case InternationalPhone(country, area, exchange, number) => 
    s"International Phone: +$country ($area) $exchange-$number"
  case _ => 
    "Invalid phone number format"
}

val phoneNumbers = List(
  "555-123-4567",
  "+1 555-123-4567",
  "+44 555-123-4567",
  "invalid-phone"
)

phoneNumbers.foreach { phone =>
  println(s"$phone -> ${formatPhone(phone)}")
}

Practical Pattern Matching Examples

JSON-like Data Processing

// Simulating JSON data with case classes
sealed trait JsonValue
case class JsonObject(fields: Map[String, JsonValue]) extends JsonValue
case class JsonArray(elements: List[JsonValue]) extends JsonValue
case class JsonString(value: String) extends JsonValue
case class JsonNumber(value: Double) extends JsonValue
case class JsonBoolean(value: Boolean) extends JsonValue
case object JsonNull extends JsonValue

def extractValue(json: JsonValue, path: String): Option[String] = {
  val pathParts = path.split("\\.").toList

  def extract(current: JsonValue, remainingPath: List[String]): Option[JsonValue] = {
    (current, remainingPath) match {
      case (value, Nil) => Some(value)
      case (JsonObject(fields), key :: rest) => 
        fields.get(key).flatMap(extract(_, rest))
      case (JsonArray(elements), indexStr :: rest) =>
        scala.util.Try(indexStr.toInt).toOption
          .filter(i => i >= 0 && i < elements.length)
          .map(elements(_))
          .flatMap(extract(_, rest))
      case _ => None
    }
  }

  extract(json, pathParts).map {
    case JsonString(s) => s
    case JsonNumber(n) => n.toString
    case JsonBoolean(b) => b.toString
    case JsonNull => "null"
    case JsonObject(_) => "[object]"
    case JsonArray(_) => "[array]"
  }
}

def analyzeJson(json: JsonValue): String = json match {
  case JsonNull => "null value"
  case JsonBoolean(true) => "boolean true"
  case JsonBoolean(false) => "boolean false"
  case JsonNumber(n) if n.isWhole => s"integer: ${n.toInt}"
  case JsonNumber(n) => f"decimal: $n%.2f"
  case JsonString(s) if s.isEmpty => "empty string"
  case JsonString(s) if s.length > 20 => s"long string (${s.length} chars): ${s.take(20)}..."
  case JsonString(s) => s"string: '$s'"
  case JsonArray(Nil) => "empty array"
  case JsonArray(List(single)) => s"single-element array: ${analyzeJson(single)}"
  case JsonArray(elements) => s"array with ${elements.length} elements"
  case JsonObject(fields) if fields.isEmpty => "empty object"
  case JsonObject(fields) => s"object with keys: ${fields.keys.mkString(", ")}"
}

// Sample JSON data
val sampleJson = JsonObject(Map(
  "name" -> JsonString("John Doe"),
  "age" -> JsonNumber(30),
  "isActive" -> JsonBoolean(true),
  "address" -> JsonObject(Map(
    "street" -> JsonString("123 Main St"),
    "city" -> JsonString("New York"),
    "zipCode" -> JsonNumber(10001)
  )),
  "hobbies" -> JsonArray(List(
    JsonString("reading"),
    JsonString("swimming"),
    JsonString("programming")
  )),
  "spouse" -> JsonNull
))

// Extract values using paths
val extractionPaths = List(
  "name",
  "age",
  "address.city",
  "address.zipCode",
  "hobbies.0",
  "hobbies.2",
  "spouse",
  "nonexistent.field"
)

extractionPaths.foreach { path =>
  extractValue(sampleJson, path) match {
    case Some(value) => println(s"$path: $value")
    case None => println(s"$path: not found")
  }
}

// Analyze different JSON values
val jsonValues = List(
  JsonNull,
  JsonBoolean(true),
  JsonNumber(42),
  JsonNumber(3.14159),
  JsonString(""),
  JsonString("Hello"),
  JsonString("This is a very long string that exceeds twenty characters"),
  JsonArray(List()),
  JsonArray(List(JsonString("single"))),
  JsonArray(List(JsonNumber(1), JsonNumber(2), JsonNumber(3))),
  JsonObject(Map()),
  JsonObject(Map("key" -> JsonString("value"), "count" -> JsonNumber(10)))
)

jsonValues.foreach { json =>
  println(analyzeJson(json))
}

State Machine with Pattern Matching

// Traffic light state machine
sealed trait TrafficLight
case object Red extends TrafficLight
case object Yellow extends TrafficLight
case object Green extends TrafficLight

case class TrafficState(light: TrafficLight, timeRemaining: Int)

def nextTrafficState(current: TrafficState): TrafficState = current match {
  case TrafficState(Red, 0) => TrafficState(Green, 30)
  case TrafficState(Green, 0) => TrafficState(Yellow, 5)
  case TrafficState(Yellow, 0) => TrafficState(Red, 25)
  case TrafficState(light, time) => TrafficState(light, time - 1)
}

def trafficLightAction(state: TrafficState): String = state match {
  case TrafficState(Red, time) => s"STOP - Red light ($time seconds remaining)"
  case TrafficState(Yellow, time) => s"CAUTION - Yellow light ($time seconds remaining)"
  case TrafficState(Green, time) if time <= 5 => s"GO (prepare to stop) - Green light ($time seconds remaining)"
  case TrafficState(Green, time) => s"GO - Green light ($time seconds remaining)"
}

// Simulate traffic light for 50 seconds
var currentState = TrafficState(Red, 10)
for (second <- 1 to 50) {
  println(f"Second $second%2d: ${trafficLightAction(currentState)}")
  currentState = nextTrafficState(currentState)
}

// Game state machine
sealed trait GameState
case object Menu extends GameState
case object Playing extends GameState
case object Paused extends GameState
case object GameOver extends GameState

sealed trait GameEvent
case object StartGame extends GameEvent
case object PauseGame extends GameEvent
case object ResumeGame extends GameEvent
case object PlayerDied extends GameEvent
case object QuitToMenu extends GameEvent

def handleGameEvent(currentState: GameState, event: GameEvent): (GameState, String) = {
  (currentState, event) match {
    case (Menu, StartGame) => (Playing, "Game started!")
    case (Playing, PauseGame) => (Paused, "Game paused")
    case (Paused, ResumeGame) => (Playing, "Game resumed")
    case (Playing, PlayerDied) => (GameOver, "Game over!")
    case (GameOver | Paused, QuitToMenu) => (Menu, "Returned to menu")
    case (Playing, QuitToMenu) => (Menu, "Quit to menu")
    case (state, event) => (state, s"Invalid action: $event not allowed in state $state")
  }
}

// Game simulation
val gameEvents = List(
  StartGame, PauseGame, ResumeGame, PlayerDied, QuitToMenu,
  StartGame, PlayerDied, StartGame, QuitToMenu
)

var gameState = Menu
println(s"Initial state: $gameState")

gameEvents.foreach { event =>
  val (newState, message) = handleGameEvent(gameState, event)
  println(s"Event: $event -> $message (State: $gameState -> $newState)")
  gameState = newState
}

Summary

In this lesson, you've mastered Scala's powerful pattern matching capabilities:

Basic Matching: Literals, types, wildcards, and guards
Collection Patterns: Lists, tuples, and sequences
Case Class Matching: Destructuring complex objects
Option/Either Patterns: Handling optional and error values
Custom Extractors: Creating your own pattern matching logic
Advanced Examples: JSON processing and state machines
Best Practices: Writing readable, maintainable pattern matches

Pattern matching is one of Scala's most expressive features, enabling you to write elegant, readable code that handles complex data structures and control flow.

What's Next

In the next lesson, we'll explore Option, Either, and Try - Scala's powerful types for handling null values, errors, and exceptions in a functional way. You'll learn how to eliminate null pointer exceptions and write more robust code.