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

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

Коментарі

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

Встановлення Visual Studio Code на Ubuntu

Visual Studio Code (VS Code) — це потужний, легкий та кросплатформний редактор коду, який став стандартом для розробників завдяки універсальності та швидкості роботи. Він підтримує Windows, Linux і macOS, запускається швидше за повноцінні IDE, але водночас може перетворюватися на повнофункціональне середовище розробки завдяки тисячам розширень. VS Code має продуману архітектуру, інтуїтивний інтерфейс, вбудований термінал та інтелектуальну систему автодоповнення на базі Language Server Protocol, що забезпечує високоякісну підтримку синтаксису для багатьох мов програмування. Серед ключових можливостей — розумне автодоповнення коду (IntelliSense), навігація між класами, символами та файлами, рефакторинг, форматування коду, інтеграція з Git, підтримка віддаленої розробки через SSH або контейнерні середовища (Dev Containers). Завдяки відлагоджувачу (debugger), який налаштовується під будь-яку мову, розробник може запускати код у контрольованому середовищі, ставити брейкпоїнти, переглядат...

Керуючі конструкції в Elixir

Elixir пропонує різноманітні керуючі конструкції для управління потоком виконання програми. На відміну від імперативних мов, всі конструкції в Elixir повертають значення, що робить код більш виразним. if та else Конструкція if перевіряє умову і виконує код відповідно до результату: # Базовий синтаксис if true do "Це буде виконано" else "Це не буде виконано" end # Однорядковий варіант if connected?, do: "Підключено", else: "Відключено" # Присвоєння результату message = if age >= 18 do "Ви повнолітній" else "Ви неповнолітній" end # Без else (повертає nil) if user_logged_in? do show_dashboard() end Примітка: В Elixir тільки false і nil вважаються хибними. Всі інші значення (включаючи 0, "", []) вважаються істинними. unless Конструкція unless — це протилежність if, виконується коли умова хибна: # ...

Шпаргалка по базових командах PostgreSQL

1. Підключення до PostgreSQL через командний рядок: psql -h <host> -p <port> -U <username> -d <database> 2. Підключення до бази без параметрів (якщо користувач і база мають однакове ім’я): psql 3. Показати список усіх баз даних: \l 4. Підключитися до іншої бази даних: \c <database_name> 5. Показати список таблиць у поточній базі: \dt 6. Показати всі об'єкти (таблиці, індекси, секвенції): \d 7. Показати таблиці з усіх схем: \dt *.* 8. Переглянути структуру конкретної таблиці: \d <table_name> 9. Виконати SQL-запит (приклад): SELECT * FROM users; 10. Вийти з psql: \q 11. Створити нову базу даних: CREATE DATABASE mydb; 12. Створити нову таблицю: CREATE TABLE users ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE ); 13. Додати новий запис: INSERT INTO users (name, email) VALUES ('Іван', 'ivan@example.com'); 14. Оновити дані в таблиці: UPDATE users SET name = 'Петро' WH...

Angular CLI

CLI (command-line interface) – інтерфейс командного рядка. Перед початком роботи має бути встановлений Node.js Встановлення: npm install -g @angular/cli Отримання допомоги: ng help Буде приблизно такий результат: add Adds support for an external library to your project. analytics Configures the gathering of Angular CLI usage metrics. See https://angular.io/cli/usage-analytics-gathering. build (b) Compiles an Angular app into an output directory named dist/ at the given output path. Must be executed from within a workspace directory. deploy Invokes the deploy builder for a specified project or for the default project in the workspace. config Retrieves or sets Angular configuration values in the angular.json file for the workspace. doc (d) Opens the official Angular documentation (angular.io) in a browser, and searches for a given keyword. e2e (e) Builds and serves an Angular app, then runs end-to-end tests. extract-i18n (i18n-extract, xi18n) Extracts i18n mes...

Дамп бази даних 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) Збирання файл...