Функции с несколькими клаузами
Самое красивое применение матчинга — определять одну функцию несколькими клаузами, каждая под свою форму аргументов. Это заменяет цепочки if/else.
Не «одна функция с разветвлениями внутри», а «несколько определений одного имени» — BEAM сама выберет подходящее по форме входа.
Вместо вложенных условий вы пишете отдельную клаузу под каждый случай, и язык диспетчеризует вызов по шаблону и охране:
defmodule Area do
def of({:circle, r}), do: 3.14159 * r * r
def of({:rect, w, h}), do: w * h
def of({:square, s}), do: s * s
end
Area.of({:circle, 2}) # => 12.566
Area.of({:rect, 3, 4}) # => 12
Так же выражают рекурсию: базовый случай — отдельная клауза, шаговый — другая. Сумма списка:
defmodule MyList do
def sum([]), do: 0
def sum([head | tail]), do: head + sum(tail)
end
MyList.sum([1, 2, 3, 4]) # => 10
Первая клауза ловит пустой список (база), вторая отрывает голову и рекурсивно идёт по хвосту.
Как работает под капотом (BEAM)
Когда вы вызываете функцию, BEAM перебирает клаузы сверху вниз, пытаясь сматчить аргументы и проверить охраны. Первая подошедшая клауза исполняется. Компилятор оптимизирует этот перебор (часто — в дерево решений), так что многоклаузные функции не медленнее ручного case. Если ни одна клауза не подошла — FunctionClauseError, что обычно сигнализирует о невалидном входе и осознанно роняет процесс (let it crash).
sum([1,2,3])
клауза sum([]) -> не совпало (список непуст)
клауза sum([h | t]) -> h=1, t=[2,3] => 1 + sum([2,3])
=> 1 + 2 + sum([3])
=> ... => 6
Та же идея на Python ▶
В Python диспетчеризацию по форме можно собрать через match-case или словарь обработчиков.
def area(shape):
match shape:
case ("circle", r): return 3.14159 * r * r
case ("rect", w, h): return w * h
case ("square", s): return s * s
print(round(area(("circle", 2)), 3)) # 12.566
print(area(("rect", 3, 4))) # 12
# Рекурсия "база + шаг" как многоклаузная sum
def my_sum(items):
if not items: # клауза sum([])
return 0
head, *tail = items # клауза sum([h | t])
return head + my_sum(tail)
print(my_sum([1, 2, 3, 4])) # 10
Частые ошибки
- Забыть базовый случай рекурсии. Без клаузы
sum([])рекурсия по списку не остановится. - Неверный порядок клауз. Слишком общий шаблон вверху перехватит вызовы, предназначенные частным клаузам.
- Ловить FunctionClauseError везде. Часто он сигнализирует о баге на входе — пусть падает, а не маскируется.
Best practices
- Выражайте варианты как отдельные клаузы — это читается как таблица случаев.
- Ставьте базовый случай рекурсии первой клаузой, шаговый — следом.
- Держите клаузы рядом и упорядочивайте от частного к общему.
Итог. Многоклаузные функции превращают ветвление в декларацию: каждая форма входа — своя строка определения. Это основа идиоматичной рекурсии и обработки {:ok, _} / {:error, _}. Дальше — модули, конвейер и устройство функций целиком.
Клаузы как таблица решений
Многоклаузные функции лучше всего воспринимать как таблицу: слева — форма входа, справа — результат. Такой код самодокументируется, потому что каждый случай виден отдельной строкой, а не утоплен в ветках if. Когда вы добавляете новый вариант обработки, вы дописываете клаузу, а не правите разросшееся условие — это снижает риск сломать существующее поведение.
Особенно элегантно это работает с тегированными кортежами результата. Функция, обрабатывающая ответ, естественно распадается на def handle({:ok, data}), do: ... и def handle({:error, reason}), do: ... — две клаузы вместо проверки тега внутри. Этот приём пронизывает всю экосистему: вы будете встречать его в обработке HTTP-ответов, чтении файлов, парсинге. Освоив диспетчеризацию по клаузам, вы фактически освоили основной способ ветвления в идиоматичном Elixir, и дальнейший код стандартной библиотеки станет читаться как родной.