Анонимные функции и захват
Функции в 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 и другими функциями высшего порядка по-настоящему лаконичной.