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

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

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...

Створення нового Elixir-проєкту

Для створення новго Elixir-проєкту можна використати команду mix new first_project --sup Зрозуміло, що Elixir має бути встановлений раніше. Пояснення команди: mix — це вбудований інструмент для управління проєктами в Elixir (аналог maven у Java чи npm у JavaScript ). new — підкоманда mix, яка створює новий проєкт. first_project — назва твого нового проєкту. Папка з цією назвою буде створена у поточному каталозі. --sup — опціональний прапорець, який додає шаблон структури з Supervision Tree. Це означає, що створений проєкт одразу буде мати структуру, яка підтримує супервізор (супервізор керує життєвим циклом процесів у системі, перезапускаючи їх при падінні). Щоб створити файл з тестом, можна запустити команду із директорії проєкту mix test Приблизний вигляд структури проєкту:

Docker-compose для створення Postgresql бази даних

Docker Compose — це інструмент, який дозволяє визначати та запускати багатоконтейнерні Docker-застосунки. Замість того, щоб вручну запускати кожен контейнер із довгими командами docker run, docker-compose.yml надає простий спосіб описати всю архітектуру додатка у вигляді YAML-файлу. Це дозволяє легко створювати, запускати, зупиняти та масштабувати сервіси за допомогою однієї команди, що значно спрощує розробку, тестування та розгортання застосунків. Основні можливості Docker Compose включають: запуск кількох контейнерів одночасно, визначення мережі та спільних томів між контейнерами, налаштування змінних середовища та автоматичне підключення сервісів один до одного через імена сервісів. Він особливо корисний для локального середовища розробки, CI/CD-процесів і навіть невеликих продакшен-рішень, де потрібно швидко відтворити середовище для тестування або демонстрації. Мінімальний docker-compose.yml для локального використання PostgreSQL без збереження даних після видалення контейне...

Шпаргалка по запуску та збірці Spring Boot-проєктів

Maven + Spring Boot 1. Збірка проєкту (із завантаженням залежностей, компіляцією, запуском тестів та створенням артефакту) mvn clean install 2. Збірка артефакту без встановлення у локальний репозиторій mvn package 3. Збірка без тестів mvn clean package -DskipTests 4. Запуск Spring Boot-проєкту mvn spring-boot:run 5. Запуск із активним профілем Spring Boot mvn spring-boot:run -Dspring-boot.run.profiles=dev 6. Запуск із параметрами mvn spring-boot:run -Dspring-boot.run.arguments="--server.port=8081 --spring.profiles.active=prod" 7. Запуск з jar-файлу java -jar target/your-app-name.jar 8. Запуск тестів mvn test 9. Запуск, якщо pom.xml у підкаталозі mvn -f шлях/до/pom.xml spring-boot:run 10. Запуск із Maven-профілем (не плутати з Spring Boot профілем) mvn clean install -P dev Gradle + Spring Boot 1. Збірка проєкту (з компіляцією, тестами та створенням jar) ...

Прості типи даних в Elixir

Мова Elixir має низку простих (примітивних) типів даних, які часто використовуються в повсякденному програмуванні. Числа Elixir підтримує цілі (integer) та дійсні числа (float). # Цілі числа a = 42 b = -7 # Дійсні числа c = 3.14 d = -0.001 Булеві значення Elixir має два булевих значення: true та false . x = true y = false z = x and y # false Атоми Атоми — це константи з іменем, що починається з двокрапки. Вони широко використовуються, наприклад, для імен параметрів або станів. :ok :error :running :elixir_is_fun Рядки Рядки в Elixir — це двійкові дані з кодуванням UTF-8, оголошуються в подвійних лапках. name = "Pavlo" greeting = "Привіт, #{name}!" Nil Nil — це спеціальне значення, що позначає "відсутність значення". value = nil is_nil(value) # true Бінарні дані та байти Бінарні дані оголошуються в подвійних лапках або як бінарні літерали. string = "Привіт" # це рядок, але також бінарні дані binary = ...