Функции с несколькими клаузами

Самое красивое применение матчинга — определять одну функцию несколькими клаузами, каждая под свою форму аргументов. Это заменяет цепочки 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, и дальнейший код стандартной библиотеки станет читаться как родной.

Проверьте себя
1. Как Elixir выбирает, какую клаузу многоклаузной функции выполнить?
AСлучайно
BПеребирает клаузы сверху вниз и берёт первую, чей шаблон и охрана совпали
CВыполняет все клаузы по очереди
DПо длине тела клаузы
2. Зачем функции рекурсии по списку нужна клауза для пустого списка?
AДля скорости
BЭто базовый случай, который останавливает рекурсию
CЧтобы обработать ошибки
DЭто необязательно