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

Модулі в 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.

Коментарі

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

Дамп бази даних Postgresql локально

Що потрібно встановити локально (Ubuntu) Встанови клієнтські утиліти PostgreSQL: pg_dump , psql , pg_restore . sudo apt update sudo apt install postgresql-client Кроки міграції Ідея: не зберігати дамп на серверах. Зробити дамп з віддаленого сервера на свій локальний ПК, потім передати його на новий сервер і там відновити. 1) Створення дампу з віддаленого сервера локально pg_dump -h OLD_SERVER_IP -U OLD_USER -Fc -d OLD_DB -f dump_file.dump Пояснення ключів: -h (хост), -U (користувач), -Fc (custom format), -d (БД), -f (вихідний файл локально). 1.1) Варіант з розбиттям на частини (якщо треба) pg_dump -h OLD_SERVER_IP -U OLD_USER -Fc -d OLD_DB | split -b 10G - dump_file.dump.part_ Це створить локально файли типу dump_file.dump.part_aa , dump_file.dump.part_ab тощо. 2) Передача дампу на новий сервер Один файл: scp dump_file.dump NEW_USER@NEW_SERVER_IP:/tmp/ Або частинами: scp dump_file.dump.part_* NEW_USER@NEW_SERVER_IP:/tmp/ 3) Збирання файл...

Основи Next.js

Що таке Next.js? Next.js — це прогресивний фреймворк для розробки веб-застосунків на основі React. Він розширює можливості React, додаючи готову інфраструктуру для Server-Side Rendering (SSR), генерації статичних сторінок, маршрутизації через файлову систему, а також можливість створювати backend-ендпоінти прямо в проєкті. Чому він став популярним? Next.js швидко здобув популярність завдяки тому, що пропонує: SEO-дружні сторінки завдяки SSR / SSG; швидкий перший рендер (First Page Load); просту маршрутизацію без налаштування; розгортання в один клік через Vercel; вбудовану оптимізацію зображень та продуктивності. Чим відрізняється від класичного React У класичному React ми отримуємо лише клієнтську бібліотеку для побудови UI-компонентів. Усе інше (SSR, маршрути, API) треба налаштовувати вручну або через сторонні бібліотеки (наприклад, React Router, Redux, Express, etc.). Next.js — це уже повноцінний фреймворк. Він одразу має: серверний рендеринг...

ООП у TypeScript

Короткий вступ TypeScript додає статичну типізацію поверх JavaScript і підтримує класичні концепти ООП: класи , успадкування , інтерфейси , інкапсуляцію , поліморфізм та абстракції . Нижче — практичні приклади та пояснення. Класи — декларація, конструктор, властивості та методи class Person { public name: string; private age: number; protected email?: string; // необов’язкове static species = 'Homo sapiens'; constructor(name: string, age: number, email?: string) { this.name = name; this.age = age; this.email = email; } public greet(): string { return `Hi, I'm ${this.name}.`; } private getBirthYear(currentYear: number): number { return currentYear - this.age; } } const p = new Person('Ivan', 30, 'ivan@example.com'); console.log(p.greet()); // console.log(p.age); // помилка: 'age' приватне console.log(Person.species); Успадкування і перевизначення (extends, super) class Employee extends Person { ...

Створення проєкту на TypeScript, модулі

Крок 1. Ініціалізація проєкту Спочатку створимо нову теку та ініціалізуємо Node.js-проєкт за допомогою npm : mkdir my-typescript-app cd my-typescript-app npm init -y Крок 2. Встановлення TypeScript Додаємо TypeScript як dev-залежність: npm install --save-dev typescript ts-node @types/node - typescript — компілятор TypeScript - ts-node — дозволяє запускати TS-файли без попередньої компіляції - @types/node — типи для стандартних Node.js API Крок 3. Створення файлу tsconfig.json Ініціалізуємо конфігурацію TypeScript: npx tsc --init Приклад файлу tsconfig.json { "compilerOptions": { "target": "ES2020", // Версія JS у вихідному коді "module": "CommonJS", // Система модулів (для Node.js) "rootDir": "src", // Вхідна папка з TypeScript-кодом "outDir": "dist", // Куди компілюється код JS "str...

Інструменти для роботи з 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...