Модулі — це основний спосіб організації коду в Elixir. Вони групують пов'язані функції разом і створюють простори імен, що дозволяє уникнути конфліктів імен та структурувати програму.
Базове визначення модуля
Модулі визначаються за допомогою ключового слова defmodule:
# Простий модуль
defmodule Greeting do
def hello do
"Привіт, світ!"
end
def hello(name) do
"Привіт, #{name}!"
end
end
# Виклик функцій модуля
Greeting.hello()
# "Привіт, світ!"
Greeting.hello("Олексій")
# "Привіт, Олексій!"
Публічні та приватні функції
Функції можуть бути публічними (def) або приватними (defp):
defmodule Calculator do
# Публічна функція - доступна ззовні
def calculate(a, b, operation) do
case operation do
:add -> add(a, b)
:subtract -> subtract(a, b)
:multiply -> multiply(a, b)
:divide -> divide(a, b)
end
end
# Приватні функції - доступні лише всередині модуля
defp add(a, b), do: a + b
defp subtract(a, b), do: a - b
defp multiply(a, b), do: a * b
defp divide(a, b) when b != 0, do: a / b
defp divide(_a, 0), do: {:error, "Ділення на нуль"}
end
# Можна викликати
Calculator.calculate(10, 5, :add)
# 15
# Не можна викликати напряму
Calculator.add(10, 5)
# ** (UndefinedFunctionError)
defp) можна викликати тільки з того ж модуля. Це допомагає інкапсулювати внутрішню логіку.
Вкладені модулі
Модулі можуть бути вкладеними для кращої організації коду:
# Вкладені модулі
defmodule MyApp do
defmodule User do
defstruct [:name, :email, :age]
def new(name, email, age) do
%__MODULE__{name: name, email: email, age: age}
end
def adult?(%__MODULE__{age: age}) do
age >= 18
end
end
defmodule Post do
defstruct [:title, :content, :author]
def new(title, content, author) do
%__MODULE__{title: title, content: content, author: author}
end
end
end
# Використання
user = MyApp.User.new("Іван", "ivan@example.com", 25)
MyApp.User.adult?(user)
# true
post = MyApp.Post.new("Заголовок", "Контент", user)
__MODULE__ — це спеціальна директива, що повертає ім'я поточного модуля. Вона корисна для уникнення дублювання імен.
Атрибути модуля
Атрибути модуля визначаються за допомогою @ і використовуються для метаданих та констант:
defmodule Config do
# Константи
@api_version "v1"
@timeout 5000
@max_retries 3
# Документація
@moduledoc """
Модуль для роботи з конфігурацією API.
"""
@doc """
Повертає базову URL для API.
"""
def api_url do
"https://api.example.com/#{@api_version}"
end
def timeout, do: @timeout
def max_retries, do: @max_retries
end
Config.api_url()
# "https://api.example.com/v1"
Config.timeout()
# 5000
Директива alias
Директива alias дозволяє створювати короткі імена для модулів:
defmodule MyApp.Services.UserService do
# Без alias
def create_user(params) do
MyApp.Schemas.User.changeset(%MyApp.Schemas.User{}, params)
end
end
defmodule MyApp.Services.UserService do
# З alias
alias MyApp.Schemas.User
def create_user(params) do
User.changeset(%User{}, params)
end
# Можна задати власне ім'я
alias MyApp.Schemas.User, as: UserSchema
def validate_user(params) do
UserSchema.validate(params)
end
# Можна використати кілька alias
alias MyApp.Schemas.{User, Post, Comment}
def get_user_content(user_id) do
user = User.get(user_id)
posts = Post.by_user(user_id)
comments = Comment.by_user(user_id)
{user, posts, comments}
end
end
Директива import
Директива import дозволяє викликати функції без префікса модуля:
defmodule StringHelper do
# Імпорт усіх функцій
import String
def process(text) do
text
|> upcase() # Замість String.upcase()
|> trim() # Замість String.trim()
|> split(",") # Замість String.split()
end
# Імпорт тільки певних функцій
import List, only: [first: 1, last: 1]
def get_boundaries(list) do
{first(list), last(list)}
end
# Імпорт усього окрім певних функцій
import Enum, except: [map: 2]
end
import обережно, оскільки він може ускладнити розуміння, звідки приходять функції. Краще використовувати alias або повні імена модулів.
Директива require
Директива require використовується для макросів:
defmodule LoggerExample do
# require потрібен для використання макросів
require Logger
def process_data(data) do
Logger.info("Обробка даних: #{inspect(data)}")
result = do_processing(data)
Logger.debug("Результат: #{inspect(result)}")
result
end
defp do_processing(data) do
# Обробка даних
data
end
end
Директива use
Директива use викликає макрос __using__ з іншого модуля:
# Визначення модуля з __using__
defmodule Validator do
defmacro __using__(_opts) do
quote do
def validate_presence(value, field) do
if is_nil(value) or value == "" do
{:error, "#{field} обов'язкове"}
else
:ok
end
end
def validate_length(value, field, min, max) do
len = String.length(value)
cond do
len < min -> {:error, "#{field} занадто коротке"}
len > max -> {:error, "#{field} занадто довге"}
true -> :ok
end
end
end
end
end
# Використання
defmodule UserValidator do
use Validator
def validate_user(%{name: name, email: email}) do
with :ok <- validate_presence(name, "Ім'я"),
:ok <- validate_length(name, "Ім'я", 2, 50),
:ok <- validate_presence(email, "Email") do
:ok
end
end
end
Поведінки (Behaviours)
Поведінки визначають набір функцій, які повинен реалізувати модуль:
# Визначення поведінки
defmodule Storage do
@callback save(key :: String.t(), value :: any()) :: :ok | {:error, term()}
@callback get(key :: String.t()) :: {:ok, any()} | {:error, term()}
@callback delete(key :: String.t()) :: :ok | {:error, term()}
end
# Реалізація поведінки
defmodule FileStorage do
@behaviour Storage
@impl Storage
def save(key, value) do
File.write("storage/#{key}", :erlang.term_to_binary(value))
end
@impl Storage
def get(key) do
case File.read("storage/#{key}") do
{:ok, binary} -> {:ok, :erlang.binary_to_term(binary)}
error -> error
end
end
@impl Storage
def delete(key) do
File.rm("storage/#{key}")
end
end
# Ще одна реалізація
defmodule MemoryStorage do
@behaviour Storage
def start_link do
Agent.start_link(fn -> %{} end, name: __MODULE__)
end
@impl Storage
def save(key, value) do
Agent.update(__MODULE__, &Map.put(&1, key, value))
:ok
end
@impl Storage
def get(key) do
case Agent.get(__MODULE__, &Map.get(&1, key)) do
nil -> {:error, :not_found}
value -> {:ok, value}
end
end
@impl Storage
def delete(key) do
Agent.update(__MODULE__, &Map.delete(&1, key))
:ok
end
end
Структури (Structs)
Структури — це спеціальні карти з фіксованим набором полів, що визначаються в модулях:
defmodule User do
# Визначення структури
defstruct [:id, :name, :email, age: 0, active: true]
# Функції для роботи зі структурою
def new(name, email) do
%__MODULE__{
id: generate_id(),
name: name,
email: email
}
end
def activate(%__MODULE__{} = user) do
%{user | active: true}
end
def deactivate(%__MODULE__{} = user) do
%{user | active: false}
end
def adult?(%__MODULE__{age: age}) when age >= 18, do: true
def adult?(%__MODULE__{}), do: false
defp generate_id do
:crypto.strong_rand_bytes(16) |> Base.encode64()
end
end
# Використання
user = User.new("Марія", "maria@example.com")
# %User{id: "...", name: "Марія", email: "maria@example.com", age: 0, active: true}
user = %{user | age: 25}
User.adult?(user)
# true
user = User.deactivate(user)
# %User{..., active: false}
Протоколи
Протоколи дозволяють визначати поліморфні функції:
# Визначення протоколу
defprotocol Renderable do
@doc "Перетворює дані в рядок для відображення"
def render(data)
end
# Реалізація для різних типів
defimpl Renderable, for: User do
def render(%User{name: name, email: email}) do
"Користувач: #{name} (#{email})"
end
end
defimpl Renderable, for: List do
def render(list) do
items = Enum.map_join(list, ", ", &Renderable.render/1)
"[#{items}]"
end
end
defimpl Renderable, for: Integer do
def render(number) do
"Число: #{number}"
end
end
# Використання
user = %User{name: "Іван", email: "ivan@example.com"}
Renderable.render(user)
# "Користувач: Іван (ivan@example.com)"
Renderable.render([1, 2, 3])
# "[Число: 1, Число: 2, Число: 3]"
Організація файлів та модулів
# Структура проєкту
lib/
my_app/
accounts/
user.ex # MyApp.Accounts.User
session.ex # MyApp.Accounts.Session
blog/
post.ex # MyApp.Blog.Post
comment.ex # MyApp.Blog.Comment
repo.ex # MyApp.Repo
my_app.ex # MyApp
# Приклад user.ex
defmodule MyApp.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
alias MyApp.Accounts.Session
schema "users" do
field :name, :string
field :email, :string
field :password_hash, :string
has_many :sessions, Session
timestamps()
end
def changeset(user, attrs) do
user
|> cast(attrs, [:name, :email, :password_hash])
|> validate_required([:name, :email])
|> validate_format(:email, ~r/@/)
|> unique_constraint(:email)
end
end
Модульна документація
defmodule MathHelper do
@moduledoc """
Допоміжні математичні функції.
## Приклади
iex> MathHelper.factorial(5)
120
iex> MathHelper.fibonacci(7)
13
"""
@doc """
Обчислює факторіал числа.
## Параметри
- n: невід'ємне ціле число
## Приклади
iex> MathHelper.factorial(0)
1
iex> MathHelper.factorial(5)
120
"""
@spec factorial(non_neg_integer()) :: pos_integer()
def factorial(0), do: 1
def factorial(n) when n > 0, do: n * factorial(n - 1)
@doc """
Обчислює n-не число Фібоначчі.
"""
@spec fibonacci(non_neg_integer()) :: non_neg_integer()
def fibonacci(0), do: 0
def fibonacci(1), do: 1
def fibonacci(n), do: fibonacci(n - 1) + fibonacci(n - 2)
@doc false
def internal_helper do
# Ця функція не буде в документації
end
end
Порівняння директив
| Директива | Призначення | Приклад |
|---|---|---|
| alias | Скорочує імена модулів | alias MyApp.User |
| import | Дозволяє викликати функції без модуля | import String |
| require | Потрібен для макросів | require Logger |
| use | Викликає __using__ макрос | use GenServer |
Кращі практики
- Один модуль — один файл: назва файлу повинна відповідати імені модуля
- Іменування: використовуйте CamelCase для модулів (MyApp.UserService)
- Організація: групуйте пов'язані модулі в папки
- Розмір: уникайте надто великих модулів (більше 300-400 рядків)
- Відповідальність: кожен модуль повинен мати одну чітку відповідальність
- Документація: документуйте всі публічні функції
- Приватні функції: використовуйте defp для внутрішньої логіки
- alias vs import: віддавайте перевагу alias над import для кращої читабельності
Модулі — це фундамент структурування коду в Elixir. Правильна організація модулів робить код зрозумілим, підтримуваним та легким для тестування. Дотримуйтесь принципів одиничної відповідальності та чіткого розділення публічного та приватного API.
Коментарі
Дописати коментар