Перейти к основному содержимому

Простые приложения на Elixir

Разработчику Архитектору

Простые приложения на Elixir

Elixir работает на BEAM — устойчивые процессы, |> (pipe), pattern matching. Скрипты запускают через elixir script.exs; для HTTP позже — Phoenix, для старта — Plug или :httpc.

Этот материал лучше проходить как мастерскую — запустили пример, изменили одну деталь, снова запустили и зафиксировали результат. Так быстрее появляется интуиция по языку.

Чтобы материал не был "набором разрозненных примеров", держите единый фокус: каждое мини-приложение тренирует одну инженерную привычку.

  • генератор паролей — работа со строками и безопасностью;
  • сортировщик — поток обработки данных и аккуратная работа с файлами;
  • калькулятор — явные контракты функций {:ok, _} / {:error, _};
  • трекер задач — сериализация и хранение состояния;
  • HTTP-пример — минимальный веб-слой и маршрутизация.

Как запускать примеры из главы

  1. Скрипт: elixir имя.exs
  2. Mix-проект: mix run или iex -S mix
  3. Нужно: Elixir 1.14+ (elixir -v).

Если команда не сработала, сверяйтесь с Первой программой на Elixir, где разобраны установка и базовая диагностика.


Генератор паролей

Как запустить

  • Файл: password.exs
  • Команда: elixir password.exs
  • Результат: пароль в stdout.
defmodule Password do
@chars ~c"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%"

def generate(len \\ 16) do
for _ <- 1..len, into: "", do: <<Enum.random(@chars)>>
end
end

IO.puts(Password.generate())

Где это пригодится на практике:

  • временные токены для локальной разработки;
  • seed-данные для тестового окружения;
  • генерация "одноразовых" строк в внутренних инструментах.

Что здесь важно:

  • ~c"..." создает charlist с кодовыми точками;
  • for ... into: "" собирает результат сразу в строку;
  • Enum.random/1 подходит для учебных задач, а для security-кейсов лучше использовать криптографически стойкий генератор (:crypto).

Версия для более надежной генерации:

def generate_secure(len \\ 16) do
bytes = :crypto.strong_rand_bytes(len)
Base.url_encode64(bytes, padding: false) |> binary_part(0, len)
end

Здесь логика проста: берём криптографически стойкие байты и преобразуем в безопасную для URL строку.


Сортировщик файла

defmodule SortFile do
def run(input, output) do
lines =
input
|> File.read!()
|> String.split("\n", trim: true)
|> Enum.sort()

File.write!(output, Enum.join(lines, "\n") <> "\n")
end
end

Это хороший учебный пример, потому что он показывает полный "data pipeline": чтение -> преобразование -> запись.

Расширение для практики:

  • добавьте String.downcase/1 перед сортировкой, чтобы сделать ее регистронезависимой;
  • замените File.read!/1 на File.read/1 и верните {:ok, result} или {:error, reason};
  • протестируйте на пустом файле и на файле из 10000 строк.

Вариант с явной обработкой ошибок:

def run_safe(input, output) do
with {:ok, bin} <- File.read(input),
lines <- String.split(bin, "\n", trim: true) |> Enum.sort(),
:ok <- File.write(output, Enum.join(lines, "\n") <> "\n") do
{:ok, length(lines)}
else
{:error, reason} -> {:error, reason}
end
end

Этот стиль особенно полезен, если дальше будете выносить обработку в отдельный процесс или API.


Калькулятор с pattern matching

defmodule Calc do
def eval(a, b, op) when is_number(a) and is_number(b) do
case op do
"+" -> {:ok, a + b}
"-" -> {:ok, a - b}
"*" -> {:ok, a * b}
"/" when b != 0 -> {:ok, a / b}
"/" -> {:error, :div_zero}
_ -> {:error, :bad_op}
end
end
end

Что этот пример учит делать правильно:

  • контракт функции читается по сигнатуре и guard;
  • ветки ошибок определены явно и предсказуемо;
  • код удобно масштабируется до "бизнес-калькуляторов" с правилами.

Почему это хороший шаблон:

  • возвращается явный результат {:ok, _} / {:error, _};
  • when is_number(a) and is_number(b) фильтрует некорректный ввод;
  • ветка "/" -> {:error, :div_zero} делает ошибку предсказуемой.

Такой стиль потом без изменений переносится в GenServer, Task и HTTP-обработчики.

Чтобы расширить пример:

  1. добавьте % и pow;
  2. разделите "парсинг операции" и "выполнение операции" на две функции;
  3. напишите 4-5 тестов через ExUnit.

Трекер задач (Jason)

# mix.exs — {:jason, "~> 1.4"}
defmodule Tasks do
defstruct [:id, :title, :done]

def load(path) do
case File.read(path) do
{:ok, bin} -> Jason.decode!(bin, keys: :atoms)
{:error, _} -> []
end
end

def save(path, tasks), do: File.write!(path, Jason.encode!(tasks, pretty: true))
end

Важно: в реальных приложениях не создавайте атомы из внешних JSON-ключей без контроля. Для начала безопаснее использовать keys: :strings, а преобразование в атомы делать только для заранее известного списка ключей.

Практический шаг вперед:

  • добавьте функцию add_task/2, которая создает %Tasks{id: ..., title: ..., done: false};
  • сделайте отдельную функцию mark_done/2;
  • храните задачи в tasks.json и вызывайте сохранение после каждого изменения.

Это уже мини-CRUD, и он хорошо подготавливает к Функциям и процессам в Elixir.

Плавное улучшение архитектуры:

  • хранение и бизнес-логика в отдельных модулях (Tasks.Store, Tasks.Service);
  • отдельная функция валидации входных данных;
  • единый тип ответа ({:ok, value} / {:error, reason}) во всех публичных функциях.

Простой HTTP (Plug.Cowboy)

Код ITЗагрузка примера кода…

Мини-контекст: это "нижний этаж" веб-стека Elixir, на котором затем строится Phoenix. Если понять этот пример, переход в полноценный фреймворк становится намного проще.

Мини-план улучшения этого примера:

  1. Добавьте маршрут get "/health" с 200.
  2. Добавьте post "/echo" и верните тело запроса.
  3. Вынесите JSON-ответ в отдельную функцию.

После этого можно переходить к Phoenix уже с пониманием, как устроен HTTP-слой.

Полезная связка с базой энциклопедии:


Характерный пример — pipe и процессы

Код ITЗагрузка примера кода…

Процессы изолированы; обмен сообщениями — основа OTP.

Что тут разобрать пошагово:

  1. start/0 создает отдельный процесс с начальными состоянием 0;
  2. :inc меняет только локальное состояние процесса;
  3. {:get, pid} отправляет текущее состояние обратно вызывающему;
  4. цикл loop/1 делает процесс долгоживущим, пока не придет команда остановки.

Именно из таких циклов собираются GenServer и далее целые OTP-деревья.


Частые ошибки и как их исправлять

СимптомПричинаЧто сделать
UndefinedFunctionErrorопечатка в имени модуля/функциипроверьте Module.function/arity
** (MatchError)жесткое сопоставление для неподходящих данныхзамените на case или with
mix не видит зависимостьне выполнен mix deps.getподтяните зависимости и перекомпилируйте
Сервер не стартует на портупорт занят другим процессомсмените порт или остановите конфликтующий сервис

Как расширять эти мини-приложения до учебных pet-проектов

Если хотите перейти от "примеров из статьи" к настоящей учебной практике, используйте правило "одна новая способность за итерацию":

  1. Итерация 1: добавить логирование и базовые тесты.
  2. Итерация 2: вынести работу с вводом/выводом в отдельный модуль.
  3. Итерация 3: добавить обработку ошибок через with.
  4. Итерация 4: завернуть stateful-часть в GenServer.
  5. Итерация 5: подключить наблюдаемость (хотя бы Logger + простые метрики).

Так вы не теряете темп и постепенно переходите к производственной структуре без резкого усложнения.


Что изучить после этой главы


См. также: Phoenix и экосистема в разделе Elixir

Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.