Модули и функции

Код в 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, окупается сторицей: рефакторить внутренности безопасно, пока контракт публичных функций цел.

Проверьте себя
1. Что идентифицирует функцию в Elixir?
AТолько имя
BИмя и арность (число аргументов), т.е. add/2 и add/3 — разные функции
CИмя и тип возврата
DПорядок объявления
2. Чем отличается defp от def?
Adefp быстрее
Bdefp создаёт приватную функцию, недоступную вне модуля
Cdefp нельзя документировать
DНичем