Как работает pattern matching в Scala?

Pattern Matching в Scala — это мощный механизм сопоставления с образцом, позволяющий проверять и распаковывать значения по структуре, типу, содержимому и даже условиям. Он часто используется вместо длинных if-else, а также при работе с case class, коллекциями, Option, алгебраическими типами данных (ADT) и другими структурами.

1. Базовый синтаксис

Сопоставление с образцом чаще всего используется с конструкцией match:

val x = 2
val result = x match {
case 1 => "one"
case 2 => "two"
case _ => "other"
}

_ — это универсальный шаблон, аналог default в других языках.

2. Pattern Matching по типу

def process(value: Any): String = value match {
case i: Int => s"Int: $i"
case s: String => s"String: $s"
case _ => "Unknown"
}

При совпадении по типу переменная автоматически кастуется, и доступна без дополнительного приведения.

3. Сопоставление с case class

Это одна из самых частых форм использования:

sealed trait Shape
case class Circle(r: Double) extends Shape
case class Rectangle(w: Double, h: Double) extends Shape
def area(shape: Shape): Double = shape match {
case Circle(r) => math.Pi \* r \* r
case Rectangle(w, h) => w \* h
}

Scala автоматически вызывает unapply, чтобы распаковать параметры case class.

4. Pattern Matching в val и аргументах

Можно деструктурировать значения при присваивании:

val (a, b) = (1, 2) // Tuple распакован
case class User(name: String, age: Int)
val User(n, a2) = User("Alice", 30)

5. Использование Option в match

val maybeName: Option\[String\] = Some("Alice")
val greeting = maybeName match {
case Some(name) => s"Hello, $name"
case None => "Hello, stranger"
}

6. Match Guards (условия)

Можно добавлять условия к шаблону:

val num = 5
val res = num match {
case x if x > 0 => "positive"
case x if x < 0 => "negative"
case _ => "zero"
}

7. Деструктуризация коллекций

val list = List(1, 2, 3)
list match {
case Nil => "empty"
case head :: tail => s"head: $head, tail: $tail"
}

Работает с :: оператором — символ конс (Cons) из списков.

8. Использование в функциях

Pattern Matching можно использовать прямо в аргументах:

def describe(opt: Option\[Int\]): String = opt match {
case Some(x) => s"Got: $x"
case None => "Empty"
}

или

val describe: Option\[Int\] => String = {
case Some(x) => s"Got: $x"
case None => "Empty"
}

9. Сопоставление с литералами, списками, кортежами

def matchList(xs: List\[Int\]) = xs match {
case List(1, 2, 3) => "exact match"
case List(1, \_\*) => "starts with 1"
case _ => "other"
}
def matchTuple(t: (Int, String)) = t match {
case (1, "a") => "matched"
case (x, y) => s"got $x and $y"
}

_* в списке означает любое количество элементов — аналог varargs.

10. Вложенные шаблоны

Можно комбинировать уровни сопоставления:

case class Address(city: String)
case class Person(name: String, address: Address)
val p = Person("Bob", Address("London"))
p match {
case Person(n, Address("London")) => s"$n lives in London"
case _ => "Unknown"
}

11. Псевдонимы @

Позволяют сохранить всё значение, удовлетворяющее шаблону:

val list = List(1, 2, 3)
list match {
case nonEmpty @ head :: tail =>
s"Full: $nonEmpty, head: $head"
case Nil => "Empty"
}

12. sealed и exhaustiveness checking

Если тип sealed, компилятор проверяет, что ты учёл все варианты:

sealed trait Expr
case class Num(n: Int) extends Expr
case class Add(a: Expr, b: Expr) extends Expr
def eval(e: Expr): Int = e match {
case Num(n) => n
case Add(a, b) => eval(a) + eval(b)
}

Если забыть вариант — компилятор может предупредить.

13. unapply и кастомные шаблоны

Можно определять собственные unapply методы:

object Even {
def unapply(x: Int): Boolean = x % 2 == 0
}
val result = 4 match {
case Even() => "Even"
case _ => "Odd"
}

14. Вложенные фильтры и кастомные паттерны

Паттерны можно комбинировать:

object Positive {
def unapply(x: Int): Option\[Int\] = if (x > 0) Some(x) else None
}
val n = 42
val res = n match {
case Positive(x) => s"positive: $x"
case _ => "non-positive"
}

15. Использование в for-comprehension

В for можно распаковывать значения:

val pairs = List((1, "a"), (2, "b"))
for ((num, str) <- pairs) {
println(s"$num -> $str")
}

16. Ошибки и поведение при несовпадении

Если ни один шаблон не подошёл, выбрасывается MatchError, если match используется без default (_):

val x = 10
val y = x match {
case 1 => "one"
// нет default, MatchError возможен
}

Pattern Matching — один из краеугольных камней Scala. Он тесно связан с типами, структурой данных, функциональными идиомами и сильно расширяет выразительность языка без необходимости в явных проверках или кастах.