Паттерн-матчинг: основы

Паттерн-матчинг — это не синтаксический сахар, а способ мышления в Elixir. Он заменяет половину привычных условий и распаковок.

Вместо «достань поле и проверь» вы описываете форму данных, и язык сам её разбирает или сообщает о несовпадении.

Слева от = вы пишете шаблон — форму, под которую должна подойти правая часть. Совпало — переменные привязались; нет — MatchError.

{:ok, value} = {:ok, 42}    # value => 42
[first | rest] = [1, 2, 3]  # first => 1, rest => [2, 3]
%{name: n} = %{name: "Ann", age: 30}  # n => "Ann"

{:ok, value} = {:error, :boom}
# ** (MatchError) no match of right hand side value: {:error, :boom}

Это и есть идиома обработки результатов: функция вернула {:ok, data} или {:error, reason}, и вы распаковываете нужную ветку.

[head | tail] = [10, 20, 30]
head   # => 10
tail   # => [20, 30]

# Игнорируем ненужное через _
{_, _, важное} = {1, 2, 3}
важное  # => 3

Как работает под капотом (BEAM)

Матчинг компилируется в эффективный код: BEAM сравнивает структуру шаблона с данными «по форме» — проверяет теги кортежей, размеры, ключи map'ов — и привязывает переменные за один проход. Для списков это особенно дёшево: [head | tail] просто берёт указатели на голову и хвост связной структуры, без копирования. Несовпадение шаблона — это нормальный, ожидаемый исход, а не дорогое исключение: на нём построены ветвления в case и выбор функций по сигнатуре.

  Шаблон:   {:ok,  value}
  Данные:   {:ok,  42}
              |      |
            тег ок?  привязать value=42
            => совпало

Та же идея на Python ▶

Современный Python умеет похожее — распаковку и structural pattern matching.

# Деструктуризация как {:ok, value} = ...
status, value = ("ok", 42)
print(status, value)            # ok 42

# [head | tail]
head, *tail = [1, 2, 3]
print(head, tail)               # 1 [2, 3]

# %{name: n} = ...
data = {"name": "Ann", "age": 30}
name = data["name"]
print(name)                     # Ann

# structural matching (Python 3.10+) — ближе всего к Elixir
def describe(pair):
    match pair:
        case ("ok", v):   return f"успех: {v}"
        case ("error", r): return f"ошибка: {r}"
print(describe(("ok", 42)))     # успех: 42
print(describe(("error", "boom")))  # ошибка: boom

Частые ошибки

  • Матчить весь map, а не часть. В Elixir %{name: n} матчит, даже если в map есть другие ключи — это частичный матч, и это нормально.
  • Забыть про MatchError. Если форма не совпала, вы получите исключение — иногда это желаемо (fail fast), иногда нужен case.
  • Путать = и ==. Первое — матч и привязка, второе — проверка равенства, возвращающая булево.

Best practices

  • Распаковывайте данные матчингом прямо в аргументах функций и в case, а не геттерами.
  • Используйте _ и _имя для игнорируемых частей — это документирует намерение.
  • Позволяйте MatchError падать там, где «несовпадение = баг»: это и есть «let it crash».

Итог. Паттерн-матчинг — это распаковка, ветвление и валидация в одном операторе. Освоив его, вы перестаёте писать ручные проверки полей. Дальше расширим его охранными выражениями и применим к выбору функций.

Матчинг как валидация формы

Отдельно стоит подчеркнуть роль матчинга как встроенной проверки контракта. Когда вы пишете {:ok, data} = fetch(), вы не просто распаковываете результат — вы заявляете «я ожидаю успех, и если его нет, это баг, пусть процесс упадёт». Это идиома fail-fast: ошибки не маскируются, а проявляются ровно в точке нарушения ожидания, с понятным стектрейсом. В мире, где есть супервизоры, такое падение безопасно и даже полезно.

Когда же несовпадение — это штатный вариант (например, элемент может быть в кэше, а может и нет), матч заворачивают в case и явно описывают обе ветки. Граница между «пусть падает» и «обработаю обе ветки» — важное проектное решение: первое для инвариантов, которые обязаны выполняться, второе для ожидаемой вариативности. Привыкайте задавать себе вопрос «несовпадение здесь — это баг или нормальный случай?» — ответ подскажет, нужен ли вам жёсткий матч или ветвление.

Проверьте себя
1. Что произойдёт при [a, b] = [1, 2, 3]?
Aa=1, b=[2,3]
BMatchError: размеры списков не совпадают
Ca=1, b=2, остаток игнорируется
Da=[1,2], b=3
2. Чем отличается %{name: n} = data от полного описания map?
AНичем
BЭто частичный матч: совпадёт, даже если в map есть и другие ключи
CТребует, чтобы в map был только ключ name
DЭто синтаксическая ошибка