Перейти до основного вмісту

Модулі в Elixir

Модулі — це основний спосіб організації коду в 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.

Коментарі

Популярні публікації

Колекції в Elixir

Списки Що таке список у Elixir У Elixir список — це впорядкована колекція елементів, реалізована як однозв’язний список . Це означає, що кожен елемент (вузол) зберігає посилання на наступний, але не на попередній. Така структура дозволяє швидко додавати елементи на початок списку, але повільно доступати до довільного елементу. У Java подібну структуру представляє LinkedList — частина Java Collections Framework. Вона реалізована як двозв’язний список, що забезпечує зручне додавання/видалення елементів з початку або кінця списку. Створення списків У Elixir список створюється за допомогою квадратних дужок: list = [1, 2, 3, 4] Додавання елементів У Elixir новий елемент можна додати тільки на початок списку за допомогою оператора | : # Elixir list = [2, 3, 4] new_list = [1 | list] # [1, 2, 3, 4] Доступ до елементів У Elixir немає прямого доступу до елементів за індексом, але це можна зробити через Enum.at : Enum.at([10, 20, 30], 1) # 20 Ітерація по списку У Elixir ...

Інструменти для роботи з Node.js

Що таке npm? npm (Node Package Manager) — це офіційний пакетний менеджер для Node.js . Він дозволяє: Встановлювати сторонні бібліотеки та фреймворки Керувати залежностями проєкту Запускати скрипти (команди) через package.json Приклад ініціалізації проєкту з npm npm init -y Файл package.json (скорочений приклад) { "name": "my-project", "version": "1.0.0", "scripts": { "start": "ts-node src/index.ts", "build": "tsc" }, "dependencies": { "express": "^4.18.0" }, "devDependencies": { "typescript": "^5.0.0", "ts-node": "^10.0.0" } } Що таке tsconfig.json? tsconfig.json — це файл конфігурації для компілятора TypeScript, який визначає, як слід компілювати код. Приклад файлу tsconfig.json { "compilerOptions": { "target": "ES2020...

Атоми в мові програмування Elixir

Атоми в Elixir Атоми є фундаментальною концепцією в Elixir , що відіграє ключову роль у створенні надійних та масштабованих систем. В Elixir це специфічний тип даних, який є константою , незмінною , ідентифікованою за своїм ім'ям . Отже, атом в Elixir — це іменована константа, що представляє себе. Уявіть, що ви даєте унікальне ім'я певній речі, і це ім'я завжди посилається саме на цю річ, і ніколи на щось інше. Наприклад, атом :ok завжди буде означати саме успішне завершення операції, а не якесь інше значення. Технічно, атоми є похідними від чисел . Кожен унікальний атом зберігається у таблиці атомів, і йому присвоюється унікальний цілочисельний ідентифікатор. Це робить їх надзвичайно ефективними для порівняння: замість порівняння рядків (що є повільною операцією), Elixir порівнює цілочисельні ідентифікатори. Переваги та особливості використання атомів Переваги атомів: Ефективність. Завдяки своєму числовому представленню, порівняння атомів є дуже швидким. Це осо...

Основи Node.js

Що таке Node.js? Node.js — це середовище виконання JavaScript поза браузером, побудоване на рушії Google V8 . Воно дозволяє запускати JavaScript на сервері, створюючи серверні застосунки з високою продуктивністю. Основні характеристики: Однопотокова модель з неблокуючим I/O Асинхронне виконання за допомогою event loop Висока продуктивність у роботі з мережевими запитами npm — найбільший реєстр пакетів Що таке event loop? Event loop — це механізм в Node.js, який дозволяє неблокуючим асинхронним операціям виконуватись у середовищі з єдиним потоком. Він постійно перевіряє наявність подій у черзі та викликає відповідні колбеки. Як працює однопоточність у Node.js? Node.js використовує один потік (main thread) для обробки JavaScript-коду. Операції, які займають час (мережеві запити, читання з файлової системи), делегуються до системних API або thread pool, і після завершення результат повертається у основний потік через event loop. Приклад: асинхронна о...

Встановлення PostgreSQL на Ubuntu-сервер

Встановлення Оновлюємо пакети та встановлюємо PostgreSQL: sudo apt update sudo apt install -y postgresql postgresql-contrib Перевіряємо статус сервісу: sudo systemctl status postgresql Якщо PostgreSQL не запущений, запустимо його: sudo systemctl start postgresql sudo systemctl enable postgresql Налаштування безпеки Зміна пароля: sudo -u postgres psql У консолі PostgreSQL: ALTER USER postgres PASSWORD 'міцний_пароль'; \q \q - вихід з консолі. Список основних команд для роботи з PostgreSQL можна переглянути за посиланням. За замовчуванням PostgreSQL слухає localhost (127.0.0.1). Щоб дозволити доступ із зовнішніх машин, редагуємо конфігурацію: sudo nano /etc/postgresql/17/main/postgresql.conf (замість 17 вкажи версію PostgreSQL, яку встановлено) Шукаємо рядок: #listen_addresses = 'localhost' та замінюємо на listen_addresses = '*' Зберігаємо (Ctrl + X, Y, Enter). Тепер редагуємо pg_hba.conf: sudo nano /etc/postgresql/17/main/pg_hba.conf...