Elixir Установка и инструментарий Elixir: asdf, Mix, Hex, форматтер, Credo, Dialyzer
0%

Установка и инструментарий Elixir: asdf, Mix, Hex, форматтер, Credo, Dialyzer

Установка и инструментарий Elixir

Хороший инструментарий в Elixir — это не «дополнительная опция», а часть культуры. mix собирает и тестирует, hex тянет зависимости, mix format устраняет споры о стиле, Credo ловит запахи, Dialyzer находит несоответствия типов, ElixirLS даёт автодополнение в редакторе. Настроим всё это правильно с первого дня — потом эти привычки сэкономят вам недели.

Что именно вы ставите

Elixir работает поверх Erlang/OTP, поэтому вам нужны обе платформы: сам Erlang/OTP (виртуальная машина BEAM) и Elixir. Версии связаны: конкретный Elixir требует минимальной версии OTP. Управлять этим руками — боль, поэтому используем менеджер версий.

Установка через asdf (рекомендуемый путь)

asdf позволяет держать разные версии Erlang и Elixir на проект — критично, когда вы ведёте несколько сервисов.

# Ставим сам asdf (пример для Linux/macOS через git)
git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.14.0
echo '. "$HOME/.asdf/asdf.sh"' >> ~/.zshrc   # для bash — ~/.bashrc
exec $SHELL

# Добавляем плагины
asdf plugin add erlang https://github.com/asdf-vm/asdf-erlang.git
asdf plugin add elixir https://github.com/asdf-vm/asdf-elixir.git

# Ставим конкретные версии (сначала Erlang, потом Elixir)
asdf install erlang 27.0
asdf install elixir 1.17.2-otp-27   # суффикс -otp-27 важен: сборка под OTP 27

# Фиксируем глобально
asdf global erlang 27.0
asdf global elixir 1.17.2-otp-27

Версии проекта фиксируются файлом .tool-versions в корне — коммитьте его, чтобы у всей команды и CI было одинаковое окружение:

erlang 27.0
elixir 1.17.2-otp-27

Проверка:

elixir --version
# Erlang/OTP 27 ... Elixir 1.17.2 (compiled with Erlang/OTP 27)

Альтернативы: на macOS многие используют brew install elixir (быстро, но одна версия), в 2024+ появился официальный установщик elixir-lang.org/install.html. Для командной работы asdf всё же удобнее из-за .tool-versions.

IEx — интерактивная оболочка

iex — ваш главный инструмент обучения и отладки. Запускается сам по себе или в контексте проекта:

iex                    # чистый REPL
iex -S mix             # REPL с загруженным проектом (все модули доступны)

Внутри полезны: h Enum.map (документация), i value (инспекция значения), recompile() (пересобрать проект без выхода), c "file.exs" (скомпилировать файл). REPL-driven разработка — норма: вы проверяете гипотезы в IEx, потом переносите в код.

Mix — сборка, задачи, окружения

Mix — это build-tool и task runner в одном флаконе (аналог cargo/npm/make). Создаём проект:

mix new my_app          # обычное приложение
mix new my_app --sup    # приложение с деревом супервизии (для боевых сервисов — почти всегда так)
cd my_app

Полезные команды:

mix deps.get            # скачать зависимости
mix compile             # скомпилировать
mix test                # прогнать тесты
mix run -e "IO.puts 42" # выполнить выражение
mix help                # список всех задач

Окружения (MIX_ENV): по умолчанию три — :dev, :test, :prod. Это влияет на конфигурацию и на то, какие зависимости подключены (only: :test и т.п.). Задаётся переменной окружения:

MIX_ENV=prod mix compile

Свои Mix-задачи пишутся как модули Mix.Tasks.MyTask — так оформляют миграции данных, генераторы и рутину.

Hex — менеджер пакетов

Hex — центральный репозиторий пакетов (как npm/PyPI). Зависимости объявляются в mix.exs:

defmodule MyApp.MixProject do
  use Mix.Project

  def project do
    [
      app: :my_app,
      version: "0.1.0",
      elixir: "~> 1.17",
      elixirc_paths: elixirc_paths(Mix.env()),
      deps: deps()
    ]
  end

  # Приложение и его OTP-зависимости, стартующие с системой
  def application do
    [extra_applications: [:logger], mod: {MyApp.Application, []}]
  end

  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_), do: ["lib"]

  defp deps do
    [
      {:jason, "~> 1.4"},                    # JSON
      {:ecto_sql, "~> 3.11"},                # БД
      {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
      {:dialyxir, "~> 1.4", only: [:dev], runtime: false},
      {:stream_data, "~> 1.0", only: [:test]}
    ]
  end
end

~> 1.4 — это семантический диапазон: «>= 1.4.0 и < 2.0.0». Точные версии фиксируются в mix.lock (коммитьте его!). Обновление — mix deps.update <pkg> или mix deps.update --all.

mix format — форматтер, отменяющий споры о стиле

В Elixir есть официальный форматтер. Никаких дискуссий про табы и переносы — есть один канонический стиль. Настройка в .formatter.exs:

[
  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
  line_length: 98
]

Команды:

mix format               # отформатировать всё
mix format --check-formatted   # упасть, если не отформатировано (для CI)

Практика: включите format-on-save в редакторе и добавьте mix format --check-formatted в CI. Форматированный код — обязательное условие ревью.

Credo — линтер и советчик по стилю

Credo ловит то, что форматтер не видит: сложные функции, потенциальные баги, антипаттерны, «код с запашком».

mix credo               # анализ
mix credo --strict      # строгий режим (рекомендуется в CI)
mix credo suggest       # с предложениями по исправлению

Конфиг генерируется в .credo.exs (mix credo.gen.config) — там включают/выключают правила под команду.

Dialyzer и typespecs — статический анализ несоответствий

Elixir динамически типизирован, но у него есть опциональные аннотации типов (@spec) и инструмент Dialyzer (через обёртку dialyxir), который методом «success typing» находит несовместимости: функцию, которая никогда не может вернуть то, что от неё ждут, недостижимый код, неверные контракты.

@spec add(integer(), integer()) :: integer()
def add(a, b), do: a + b

@type user :: %{name: String.t(), age: non_neg_integer()}
@spec greet(user()) :: String.t()
def greet(%{name: name}), do: "Привет, #{name}"
mix dialyzer            # первый запуск строит PLT (кэш типов OTP) — это долго, потом быстро

Dialyzer не находит все ошибки типов (он консервативен: сообщает только о точно невозможном), но пойманные им проблемы — почти всегда реальные баги. Держите PLT в кэше CI.

В экосистеме появляется постепенная типизация в самом компиляторе (проект «set-theoretic types», Elixir 1.17+). Следите за dashbit.co/blog — это меняет ландшафт статических проверок.

LSP / IDE

  • VS Code: расширение ElixirLS — автодополнение, переход к определению, инлайн-Dialyzer, форматирование. Появляется и более новый Lexical, и Expert — можно попробовать, если ElixirLS тормозит.
  • Neovim: elixir-tools.nvim или ElixirLS через nvim-lspconfig.
  • Другие: плагины есть для IntelliJ, Emacs.

Минимальный набор ожиданий от LSP: go-to-definition, hover-документация, автодополнение функций модуля, подсветка ошибок компиляции.

Структура проекта

mix new my_app --sup создаёт каноничный layout:

my_app/
├── .formatter.exs        # правила форматтера
├── .tool-versions        # версии Erlang/Elixir (asdf)
├── mix.exs               # манифест проекта: зависимости, приложение
├── mix.lock              # зафиксированные версии (коммитить!)
├── config/
│   ├── config.exs        # общая конфигурация (compile-time)
│   ├── dev.exs           # переопределения для dev
│   ├── test.exs
│   ├── prod.exs
│   └── runtime.exs       # runtime-конфигурация (читается при старте релиза!)
├── lib/
│   ├── my_app.ex         # публичный «фасад» приложения
│   ├── my_app/
│   │   └── application.ex # дерево супервизии — точка входа OTP-приложения
├── test/
│   ├── test_helper.exs   # ExUnit.start()
│   └── my_app_test.exs

Ключевые принципы организации lib/:

  • Один модуль — один файл, путь файла отражает вложенность модуля: MyApp.Accounts.Userlib/my_app/accounts/user.ex.
  • Бизнес-логику группируйте по контекстам (MyApp.Accounts, MyApp.Billing) — об этом в статье про архитектуру.
  • application.ex описывает, что стартует и под каким супервизором — сердце OTP-приложения.
# lib/my_app/application.ex
defmodule MyApp.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      # процессы, которые стартуют вместе с приложением
      # MyApp.Repo,
      # {Phoenix.PubSub, name: MyApp.PubSub},
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

config.exs vs runtime.exs — важное различие

  • config/config.exs и config/{dev,test,prod}.exs читаются на этапе компиляции. Секреты сюда класть нельзя — они «впекутся» в артефакт.
  • config/runtime.exs читается при старте приложения/релиза. Именно здесь читают переменные окружения (System.get_env/1) — URL базы, секретные ключи. Это правильное место для всего, что зависит от среды исполнения.
# config/runtime.exs
import Config

if config_env() == :prod do
  config :my_app, MyApp.Repo,
    url: System.fetch_env!("DATABASE_URL"),
    pool_size: String.to_integer(System.get_env("POOL_SIZE", "10"))
end

Подробнее о конфигурации и релизах — в статье Деплой и наблюдаемость.

Чек-лист «здорового» проекта

  • .tool-versions зафиксирован и в CI, и локально.
  • mix format --check-formatted в CI.
  • mix credo --strict в CI.
  • mix dialyzer с кэшированием PLT в CI.
  • mix test --cover и порог покрытия.
  • Секреты только в runtime.exs через переменные окружения.

Источники

Что дальше

Основы языка — синтаксис, типы, структуры данных и паттерн-матчинг.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов