Функции, модули и многовариантные определения
Как устроены функции — главный строительный кирпич Erlang.
Клауза (clause) — один вариант определения функции с собственным шаблоном аргументов. Функция может состоять из нескольких клауз.
В Erlang нет привычного if/else как основного инструмента ветвления. Вместо этого функцию определяют несколькими клаузами, и нужная выбирается по pattern matching аргументов. Это делает код декларативным и читаемым: вместо того чтобы внутри одного тела функции громоздить лестницу условий, вы перечисляете отдельные случаи как самостоятельные определения. Каждая клауза отвечает на вопрос «что делать, если аргументы выглядят вот так», и читать такую функцию можно как таблицу вариантов, а не как запутанный поток управления.
Этот стиль глубоко влияет на то, как вы проектируете программы. Вы начинаете мыслить в терминах «какие бывают формы входных данных и какая реакция нужна на каждую», а не «какую переменную проверить первой». Получившийся код ближе к спецификации задачи, чем к рецепту её механического выполнения, и потому легче читается, проверяется и сопровождается. Опытные разработчики на Erlang специально стараются выразить логику через клаузы и сопоставление, прибегая к явным условным конструкциям лишь там, где иначе никак.
Несколько клауз
Клаузы одной функции разделяются точкой с запятой, а последняя завершается точкой. Erlang выбирает первую клаузу, чей шаблон совпал с аргументами. Все клаузы обязаны иметь одно и то же имя и одинаковое число аргументов — иначе компилятор сочтёт их разными функциями. Эта пара «имя и арность» (число аргументов) и есть полное имя функции в Erlang; функции с одинаковым именем, но разной арностью считаются совершенно разными.
-module(traffic).
-export([action/1]).
action(red) -> "стоп";
action(yellow) -> "приготовиться";
action(green) -> "ехать".
1> traffic:action(green).
"ехать"
2> traffic:action(red).
"стоп"
Порядок клауз так же важен, как и в case: язык идёт сверху вниз и берёт первую подошедшую. Поэтому более частные случаи ставят выше, а более общие — ниже. Если общая клауза с переменной-аргументом окажется первой, она перехватит любой вызов, и специальные клаузы под ней станут мёртвым кодом. Компилятор обычно предупреждает о такой недостижимой клаузе, и к этим предупреждениям стоит относиться серьёзно.
Модуль как единица кода
Функции в Erlang не висят в пустоте — они всегда живут внутри модуля. Модуль объявляется директивой -module(имя) в начале файла, и имя модуля обязано совпадать с именем файла. Это простое правило избавляет от путаницы: зная имя модуля, вы всегда знаете, в каком файле искать его код. Вызов функции из другого модуля записывается как модуль:функция(аргументы) — двоеточие явно указывает, чей именно код вы зовёте.
Такая дисциплина делает структуру программы прозрачной. Модуль становится естественной границей ответственности: внутри него собраны функции, решающие одну задачу, а наружу торчит лишь небольшой набор точек входа. Когда программа разрастается, эта организация помогает удерживать сложность под контролем — каждый модуль можно осмыслить отдельно, не держа в голове весь проект целиком.
Локальные и экспортируемые функции
Только функции из списка -export видны снаружи модуля. Остальные — приватные помощники. Это даёт инкапсуляцию: внешний мир зависит от узкого интерфейса, а внутренности можно менять свободно. Пока вы сохраняете поведение экспортированных функций, вы вольны как угодно переписывать приватные вспомогательные функции, переименовывать их, дробить и объединять — ни один внешний код от этого не сломается, потому что он попросту не имел к ним доступа.
Полезно с самого начала относиться к списку -export как к публичному контракту модуля. Чем он короче, тем меньше у вас обязательств перед остальной системой и тем свободнее вы внутри. Распространённая ошибка новичка — экспортировать всё подряд «на всякий случай»; это размывает границу между интерфейсом и реализацией и со временем превращает любой внутренний рефакторинг в риск что-нибудь сломать у пользователей модуля.
-module(geometry).
-export([circle_area/1]).
circle_area(R) ->
pi() * R * R.
pi() -> 3.14159. % приватная, наружу не видна
Анонимные функции (fun)
Функции в Erlang — значения первого класса: их можно передавать в другие функции и хранить в переменных. Анонимная функция записывается через fun ... end. Это открывает целый стиль программирования, где поведение передаётся так же легко, как число или строка. Вы можете завести функцию, которая принимает другую функцию и применяет её к каждому элементу списка, и тем самым один раз написать обход, а способ обработки подставлять под задачу. Именно на этой идее построены ключевые функции модуля lists, с которым мы скоро познакомимся подробнее.
Double = fun(X) -> X * 2 end,
Result = Double(21). % 42
% Передаём функцию в lists:map
Doubled = lists:map(fun(X) -> X * 2 end, [1, 2, 3]).
% [2, 4, 6]
Замыкания
Анонимная функция запоминает переменные из окружения — это замыкание. В примере ниже функция-результат «несёт с собой» значение N, которое было видно в момент её создания, и продолжает помнить его даже после того, как породившая функция завершилась. Замыкание словно фотографирует нужные переменные и хранит снимок внутри себя.
make_adder(N) ->
fun(X) -> X + N end.
% AddTen = make_adder(10), AddTen(5) даст 15
Замыкания позволяют изящно настраивать поведение под конкретные нужды. Одна функция-фабрика порождает целое семейство специализированных функций: make_adder(10) даёт «прибавлятель десятки», make_adder(100) — «прибавлятель сотни», и каждая помнит свой захваченный параметр. Это частый приём, когда нужно передать куда-то функцию с уже «зашитыми» настройками, не заводя для них отдельных переменных и не прокидывая их через все промежуточные вызовы.
Как работает под капотом
Выбор клаузы по аргументам компилируется в то же дерево решений, что и case, — это быстро. Анонимные функции компилируются в специальные значения (closures), которые несут с собой ссылку на код и захваченные переменные. Поскольку данные неизменяемы, захват переменной безопасен: замыкание видит ровно то значение, что было на момент создания. В языках с изменяемыми переменными замыкания таят ловушку — захваченная переменная может неожиданно поменяться у вас за спиной, и функция начнёт видеть совсем не то, что вы ожидали. В Erlang этой проблемы нет в принципе: захваченное значение зафиксировано навсегда, и рассуждать о поведении замыкания просто.
Неизменяемость данных вообще пронизывает всю эту тему. Раз связанную переменную нельзя переприсвоить, функция, получив аргументы, не может незаметно их испортить; раз замыкание захватывает значение, а не «ячейку», оно не зависит от того, что происходит снаружи после его создания. Эти гарантии — не мелкая деталь, а фундамент, на котором позже держится безопасная конкурентность: процессы, не имеющие общей изменяемой памяти, не могут устроить гонку за данные.
Частые ошибки
- Разделять клаузы точкой вместо точки с запятой. Точка завершает определение функции целиком.
- Забыть экспортировать функцию. Тогда её не вызвать как
module:fun(...). - Ожидать, что fun изменит захваченную переменную. Захваченные данные неизменяемы.
Итоги
- Функция состоит из клауз; нужная выбирается по pattern matching аргументов.
- Клаузы разделяются
;, всё определение завершается.. - Снаружи видны только функции из
-export— это инкапсуляция. - Анонимные функции
fun ... end— значения первого класса и замыкания.