Простые приложения на Elixir
Простые приложения на Elixir
Elixir работает на BEAM — устойчивые процессы, |> (pipe), pattern matching. Скрипты запускают через elixir script.exs; для HTTP позже — Phoenix, для старта — Plug или :httpc.
Этот материал лучше проходить как мастерскую — запустили пример, изменили одну деталь, снова запустили и зафиксировали результат. Так быстрее появляется интуиция по языку.
Чтобы материал не был "набором разрозненных примеров", держите единый фокус: каждое мини-приложение тренирует одну инженерную привычку.
- генератор паролей — работа со строками и безопасностью;
- сортировщик — поток обработки данных и аккуратная работа с файлами;
- калькулятор — явные контракты функций
{:ok, _}/{:error, _}; - трекер задач — сериализация и хранение состояния;
- HTTP-пример — минимальный веб-слой и маршрутизация.
Как запускать примеры из главы
- Скрипт:
elixir имя.exs - Mix-проект:
mix runилиiex -S mix - Нужно: 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-обработчики.
Чтобы расширить пример:
- добавьте
%иpow; - разделите "парсинг операции" и "выполнение операции" на две функции;
- напишите 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. Если понять этот пример, переход в полноценный фреймворк становится намного проще.
Мини-план улучшения этого примера:
- Добавьте маршрут
get "/health"с200. - Добавьте
post "/echo"и верните тело запроса. - Вынесите JSON-ответ в отдельную функцию.
После этого можно переходить к Phoenix уже с пониманием, как устроен HTTP-слой.
Полезная связка с базой энциклопедии:
Характерный пример — pipe и процессы
Код ITЗагрузка примера кода…
Процессы изолированы; обмен сообщениями — основа OTP.
Что тут разобрать пошагово:
start/0создает отдельный процесс с начальными состоянием0;:incменяет только локальное состояние процесса;{:get, pid}отправляет текущее состояние обратно вызывающему;- цикл
loop/1делает процесс долгоживущим, пока не придет команда остановки.
Именно из таких циклов собираются GenServer и далее целые OTP-деревья.
Частые ошибки и как их исправлять
| Симптом | Причина | Что сделать |
|---|---|---|
UndefinedFunctionError | опечатка в имени модуля/функции | проверьте Module.function/arity |
** (MatchError) | жесткое сопоставление для неподходящих данных | замените на case или with |
mix не видит зависимость | не выполнен mix deps.get | подтяните зависимости и перекомпилируйте |
| Сервер не стартует на порту | порт занят другим процессом | смените порт или остановите конфликтующий сервис |
Как расширять эти мини-приложения до учебных pet-проектов
Если хотите перейти от "примеров из статьи" к настоящей учебной практике, используйте правило "одна новая способность за итерацию":
- Итерация 1: добавить логирование и базовые тесты.
- Итерация 2: вынести работу с вводом/выводом в отдельный модуль.
- Итерация 3: добавить обработку ошибок через
with. - Итерация 4: завернуть stateful-часть в
GenServer. - Итерация 5: подключить наблюдаемость (хотя бы
Logger+ простые метрики).
Так вы не теряете темп и постепенно переходите к производственной структуре без резкого усложнения.
Что изучить после этой главы
- Первая программа на Elixir — если нужно заново пройти базовый запуск;
- Типы данных и неизменяемость — чтобы увереннее работать с
%{},[],{}; - Управляющие конструкции и операторы Elixir — для
case,with, guards; - Архитектура на базе Erlang VM (BEAM) — чтобы понимать, почему процессы так устроены.
См. также: Phoenix и экосистема в разделе Elixir
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.