Анонимные функции и захват

Функции в Elixir — значения первого класса: их можно класть в переменные, передавать в другие функции и возвращать. Знакомимся с анонимными функциями и оператором захвата.

Если функция — это данные, то «передать поведение» так же естественно, как передать число. На этом стоит вся работа с коллекциями.

Анонимную функцию создают через fn ... end и вызывают с точкой:

add = fn a, b -> a + b end
add.(2, 3)        # => 5  (точка обязательна!)

double = fn x -> x * 2 end
double.(21)       # => 42

Для краткости есть оператор захвата &: &1, &2 — позиционные аргументы, а &Mod.fun/arity захватывает именованную функцию:

double = &(&1 * 2)
double.(21)                 # => 42

# Захват именованной функции
upcase = &String.upcase/1
upcase.("hi")               # => "HI"

# Передаём поведение в функцию высшего порядка
Enum.map([1, 2, 3], &(&1 * 10))   # => [10, 20, 30]

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

Анонимная функция компилируется в замыкание: она захватывает значения переменных из окружения в момент создания. Поскольку данные неизменяемы, захваченные значения никогда не «протухнут» — никаких сюрпризов с изменившейся переменной, как бывает в языках с мутацией. Внутри VM замыкание — это специальная структура (fun), хранящая ссылку на код и захваченные значения. Вызов через fun.() отличается от вызова именованной функции тем, что адрес кода берётся из этой структуры, отсюда и обязательная точка в синтаксисе.

  multiplier = 10
  f = fn x -> x * multiplier end
       └── замыкание захватывает multiplier=10 (навсегда,
           т.к. значение неизменяемо)
  f.(5)  => 50

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

В Python анонимные функции — это lambda, а замыкания работают схоже.

add = lambda a, b: a + b
print(add(2, 3))               # 5

double = lambda x: x * 2
print(double(21))              # 42

# Функция высшего порядка: передаём поведение
print(list(map(lambda x: x * 10, [1, 2, 3])))   # [10, 20, 30]

# Замыкание захватывает переменную окружения
def make_multiplier(factor):
    return lambda x: x * factor

times3 = make_multiplier(3)
print(times3(5))               # 15

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

  • Забыть точку. add(2, 3) для анонимной функции не сработает — нужно add.(2, 3).
  • Перегружать &. Захват хорош для простых выражений; сложную логику читаемее писать через fn ... end.
  • Путать арность в захвате. &String.upcase/1 требует точного числа аргументов; неверная арность — ошибка.

Best practices

  • Для коротких преобразований внутри pipe используйте &(&1 ...) — компактно и читаемо.
  • Для логики из нескольких выражений предпочитайте fn ... end.
  • Захватывайте именованные функции (&Mod.fun/1), когда поведение уже определено — не дублируйте обёртки.

Итог. Функции-значения и оператор захвата делают Elixir по-настоящему функциональным: поведение передаётся так же легко, как данные. Это фундамент модуля Enum, к которому мы переходим в следующем разделе.

Функции как строительный материал

То, что функции — значения первого класса, открывает целый стиль программирования. Вы можете хранить функции в map'е, выбирая поведение по ключу (диспетчер вместо длинного case), возвращать функцию из функции (фабрики поведения), собирать конвейеры динамически. Это не экзотика, а повседневный инструмент: коллбэки в библиотеках, стратегии сортировки, валидаторы — всё это функции, передаваемые как данные.

Полезно различать две формы захвата по назначению. Запись &(&1 * 2) создаёт новую анонимную функцию из выражения — она удобна для одноразовых преобразований внутри pipe. А запись &String.upcase/1 не создаёт ничего нового, а лишь «берёт ссылку» на уже существующую именованную функцию нужной арности. Второе предпочтительнее, когда поведение уже реализовано: вы не плодите обёртки, а переиспользуете готовое. Эта пара приёмов делает работу с Enum и другими функциями высшего порядка по-настоящему лаконичной.

Проверьте себя
1. Как вызвать анонимную функцию, лежащую в переменной f?
Af(args)
Bf.(args) — с точкой
Ccall(f, args)
Df->args
2. Что захватывает анонимная функция при создании?
AНичего
BЗначения переменных из окружения; благодаря неизменяемости они не «протухнут»
CТолько глобальные переменные
DСсылки на изменяемые ячейки