Модули и функции
Код в Elixir живёт в модулях, а работу делают функции. Никаких классов — только группировка функций по смыслу.
Модуль — это пространство имён для функций. Состояние сюда не помещают: оно живёт в процессах, а функции лишь преобразуют данные.
Модуль объявляют через defmodule, функции — через def (публичные) и defp (приватные):
defmodule Calculator do
@moduledoc "Простые арифметические операции."
def add(a, b), do: a + b
def double(n), do: add(n, n)
defp secret_factor, do: 42 # видна только внутри модуля
end
Calculator.add(2, 3) # => 5
Calculator.double(10) # => 20
Функции различаются именем и арностью (числом аргументов): add/2 и add/3 — разные функции. Длинная и короткая формы эквивалентны:
def add(a, b) do
a + b
end
def add(a, b), do: a + b # то же самое, короткая форма
Как работает под капотом (BEAM)
Каждый модуль компилируется в отдельный .beam-файл и загружается в VM по имени. Функция идентифицируется тройкой Module/Function/Arity (MFA) — именно так BEAM их вызывает и так они отображаются в стектрейсах. Атрибуты вроде @moduledoc и @doc сохраняются в скомпилированный байткод, поэтому справка доступна в iex (h Calculator.add) и в сгенерированной документации. Приватные функции (defp) не экспортируются — их просто нет в таблице экспортируемых функций модуля.
defmodule Calculator -> Calculator.beam
экспортируются: add/2, double/1
скрыты (defp): secret_factor/0
Вызов = MFA: {Calculator, :add, 2}
Та же идея на Python ▶
Модуль Elixir ближе всего к модулю Python (файлу с функциями), а не к классу.
# "Модуль" как пространство имён функций
def add(a, b):
return a + b
def double(n):
return add(n, n)
def _secret_factor(): # подчёркивание ~ defp (приватная по соглашению)
return 42
print(add(2, 3)) # 5
print(double(10)) # 20
# Арность важна: разные сигнатуры -> разное поведение
def describe(a, b=None):
return f"один: {a}" if b is None else f"два: {a},{b}"
print(describe(1)) # один: 1
print(describe(1, 2)) # два: 1,2
Частые ошибки
- Искать классы и поля. В модуле нет состояния; данные передаются аргументами и возвращаются.
- Путать арности.
add/2иadd/3независимы; вызов с неверным числом аргументов не найдёт функцию. - Звать
defpснаружи. Приватная функция недоступна вне модуля — будет ошибка undefined.
Best practices
- Группируйте по смыслу: один модуль — одна ответственность.
- Прячьте детали в
defp, оставляя в публичном API минимум функций. - Документируйте публичные функции через
@docи модуль — через@moduledoc.
Итог. Модули организуют функции, функции преобразуют данные, состояние сюда не вмешивается. Это чистая функциональная организация кода. Дальше — оператор |>, который связывает функции в читаемые конвейеры.
Атрибуты модуля и организация кода
Помимо @moduledoc и @doc, у модулей есть атрибуты, играющие роль «констант времени компиляции»: @pi 3.14159 вычисляется один раз при компиляции и подставляется в места использования. Это не переменная — это способ дать имя постоянному значению без рантайм-накладных расходов. Атрибуты также используются для накопления метаданных и конфигурации, которые библиотеки потом читают через интроспекцию.
По мере роста проекта модули организуют в иерархию имён через точку: MyApp.Accounts, MyApp.Accounts.User. Это чисто соглашение об именовании пространств (никакого «наследования»), но оно структурирует код по доменам. В Phoenix-приложениях это вырастает в паттерн «контекстов» — модулей-фасадов вроде Accounts, скрывающих детали работы с данными за чистым публичным API. Привычка держать публичную поверхность модуля узкой, а детали — в defp, окупается сторицей: рефакторить внутренности безопасно, пока контракт публичных функций цел.