Что такое pattern matching и где его можно применять?

Pattern Matching (сопоставление с образцом) в Elixir — это один из центральных механизмов языка, который позволяет не просто присваивать значения, а деконструировать, проверять структуру и извлекать данные из сложных структур в элегантной и выразительной форме. Это мощный инструмент, который заменяет традиционные if, switch, try-catch и многие другие конструкции, встречающиеся в императивных языках.

Основы сопоставления с образцом

В Elixir знак = — не оператор присваивания, как, например, в JavaScript или Python. Это оператор сопоставления: он проверяет, можно ли левую часть уравнять правой, и, если да, извлекает данные.

{:ok, result} = {:ok, 42} # result = 42

Если структура слева не совпадает со структурой справа, произойдёт ошибка сопоставления (MatchError):

{:ok, result} = {:error, "ошибка"} # Ошибка

Где применяется Pattern Matching

1. Присваивание переменных

\[a, b, c\] = \[1, 2, 3\]
\# a = 1, b = 2, c = 3
%{name: n} = %{name: "Alex", age: 32}
\# n = "Alex"

Позволяет извлекать значения из списков, кортежей, словарей, структур и других вложенных объектов.

2. В функциях (определение по шаблону)

Функции могут иметь разные варианты (клаузы) в зависимости от входных данных:

defmodule Math do
def double(0), do: 0
def double(x), do: x \* 2
end

При вызове Math.double(0) сработает первая функция, при Math.double(5) — вторая.

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

case {:ok, 200} do
{:ok, code} -> "Успешно: #{code}"
{:error, \_} -> "Ошибка"
end

Очень удобно для обработки результатов, ошибок, структурированных данных.

4. Сопоставление в with

with {:ok, token} <- get_token(),
{:ok, user} <- get_user(token),
{:ok, result} <- do_something(user) do
result
else
{:error, reason} -> {:error, reason}
end

with позволяет последовательно обрабатывать цепочки действий, в каждой из которых ожидается определённая форма результата. Если хоть одно сопоставление не удаётся, выполнение переходит в else.

5. Сопоставление в for, Enum, Stream

Enum.map(\[{1, "a"}, {2, "b"}\], fn {num, str} ->
"#{num}-#{str}"
end)

Можно сразу распаковывать кортежи, списки, структуры, что повышает читаемость и упрощает код.

6. Сопоставление в receive (актерная модель)

receive do
{:msg, content} -> IO.puts("Сообщение: #{content}")
{:error, reason} -> IO.puts("Ошибка: #{reason}")
end

Внутри receive используется pattern matching для обработки сообщений, поступающих в процесс.

7. Guard-условия + Pattern Matching

Можно применять шаблоны в сочетании с ограничениями (when):

def process(0), do: :zero
def process(n) when is_integer(n) and n > 0, do: :positive
def process(n) when is_integer(n) and n < 0, do: :negative

Это особенно полезно для фильтрации значений и определения логики.

8. Работа с Map и Struct

defmodule User do
defstruct name: "", age: 0
end
%User{name: name} = %User{name: "John", age: 30}
\# name = "John"

Сопоставление можно использовать и со структурами, где тип структуры — часть шаблона.

Особенности

  • Одноразовое сопоставление: переменная может быть сопоставлена один раз в выражении, но если она уже определена — повторное сопоставление проверяет совпадение:
x = 5
5 = x # OK
6 = x # MatchError
  • ^ (пин-переменная): используется для фиксации значения уже существующей переменной:
x = 10
^x = 10 # OK
^x = 20 # MatchError

Ошибки при сопоставлении

Если шаблон слишком строгий и ожидает конкретный формат, но получает другой — произойдёт ошибка:

\[a, b\] = \[1\] # MatchError
%{id: x} = %{name: "Bob"} # MatchError

Поэтому сопоставление часто сочетается с case, with, try, чтобы обрабатывать исключения.

Применимость в реальных задачах

  • Обработка {:ok, val} / {:error, reason}

  • Извлечение значений из вложенных структур (JSON, API)

  • Упрощение ветвлений

  • Чтение и разбор файлов (например, CSV, JSON, XML)

  • Упрощение кода в микросервисах, работающих через сообщения и процессы

  • API-пайплайны, проверки и валидации данных