Функции, модули и многовариантные определения

Как устроены функции — главный строительный кирпич 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 — значения первого класса и замыкания.
Проверьте себя
1. Как Erlang выбирает нужную клаузу функции?
AПо случайному выбору
BПо сопоставлению шаблонов аргументов, сверху вниз — первая совпавшая
CПо имени переменной
DПо длине кода
2. Какие функции видны снаружи модуля?
AВсе
BТолько перечисленные в -export
CТолько первая
DТолько анонимные
3. Что такое замыкание (closure) в Erlang?
AЗакрытие файла
BАнонимная функция, запоминающая переменные из окружения
CКонец модуля
DСпособ завершить процесс