BEAM давно ценят за лёгкие процессы и умение переживать падения отдельных компонентов. Но динамическая типизация в Erlang и Elixir оставляет много ошибок на потом: опечатка в атоме или неверная форма сообщения всплывает в рантайме. Gleam добавляет строгую статическую систему типов, сохраняя модель процессов и совместимость с OTP. Компилятор написан на Rust и выдаёт Erlang-байткод или JavaScript.
Что именно даёт статическая типизация
Gleam проверяет все ветви case на полноту, запрещает null и требует явно обрабатывать ошибки через Result. Компилятор знает форму данных на каждом шаге, поэтому цепочка преобразований не разваливается в момент запроса. При этом среда выполнения остаётся BEAM — те же процессы, супервизоры и наблюдаемость.
Язык не пытается быть Erlang с другим синтаксисом. В нём нет макросов и динамических атомов из строки, зато есть алгебраические типы, обобщения и удобный pipeline. Для задач, где важна предсказуемость, это выигрыш; для метапрограммирования — ограничение.
Экосистема небольшая, но растущая: в Hex есть HTTP-серверы, драйверы Postgres, JSON и тестовые библиотеки. Пакеты на Gleam можно вызывать из Erlang и наоборот, поэтому зрелые библиотеки не приходится переписывать. Компилятор выдаёт понятные ошибки с номерами строк, а форматирование встроено в утилиту и не требует отдельного линтера.
Установка и первый проект
Компилятор ставится одним скриптом, дальше проект создаётся встроенной утилитой. Она же генерирует структуру, тесты и файл зависимостей.
curl -fsSL https://gleam.run/install.sh | sh
gleam new gateway
cd gateway
gleam run
В gleam.toml указывают имя, версию и цель сборки. По умолчанию target = «erlang»; для фронтенда или edge-среды переключают на javascript. Зависимости добавляют командой gleam add, они публикуются в Hex.
gleam add gleam_otp
gleam add wisp
gleam build
gleam test
Типы и разбор вариантов
Вместо набора констант описывают собственный тип с параметрами. Компилятор заставит обработать каждый вариант, а забытую ветку покажет как ошибку, а не как исключение в проде.
import gleam/string
pub type Status {
Active
Suspended(reason: String)
Deleted(at: Int)
}
pub fn describe(status: Status) -> String {
case status {
Active -> "active"
Suspended(reason) -> string.concat(["suspended: ", reason])
Deleted(_) -> "deleted"
}
}
Списки и кортежи неизменяемы, а рекурсия заменяет циклы. Это непривычно после императивных языков, но хорошо ложится на обработку сообщений и обход деревьев.
Обобщения позволяют писать функции, не привязанные к конкретному типу: например, обёртку над Result или разбор списка. Компилятор выводит параметры сам, поэтому аннотации нужны не всегда. Если тип вывести нельзя, ошибка появится на сборке, а не в момент выполнения.
Ошибки как значения
Result — основной способ вернуть неуспех. Никаких исключений для ожидаемых ситуаций: вызывающий обязан решить, что делать. Оператор конвейера передаёт значение в следующий вызов и делает цепочки читаемыми.
import gleam/int
pub fn parse_port(raw: String) -> Result(Int, String) {
case int.parse(raw) {
Ok(port) if port > 1024 -> Ok(port)
Ok(_) -> Error("port too low")
Error(_) -> Error("not a number")
}
}
Код не скомпилируется, пока не обработаны оба исхода. В связке с pattern matching это убирает целый класс падений, которые в динамическом языке ловят только тестами.
Процессы и OTP
Поверх BEAM доступны акторы из gleam_otp. Актор — это процесс с состоянием и функцией обработки сообщений; супервизор перезапускает его при сбое. Типизированные сообщения убирают ошибки, знакомые по «сырым» атомам.
import gleam/otp/actor
pub fn start() -> Result(actor.Started(Int), actor.StartError) {
actor.start(0, handle)
}
fn handle(state: Int, delta: Int) -> actor.Next(Int, Int) {
actor.continue(state + delta)
}
Точные сигнатуры стоит сверять с документацией пакета: API развивается. Смысл остаётся прежним: состояние явное, сообщение типизировано, а поведение при падении задаёт супервизор.
Дерево супервизоров строят так же, как в Erlang: дочерние процессы получают стратегию перезапуска и лимит попыток. Если актор падает из-за ошибки в данных, супервизор перезапустит его с чистым состоянием, а не уронит весь сервис. Для долгоживущих соединений это важнее, чем скорость обработки одного запроса.
Веб-сервис и интероп
Для HTTP обычно берут wisp и mist: первый даёт удобные запросы и ответы, второй — сервер. Обработчик остаётся обычной функцией, которую легко тестировать без сети.
import wisp
pub fn handle(request: wisp.Request) -> wisp.Response {
case wisp.path_segments(request) {
["health"] -> wisp.ok()
_ -> wisp.not_found()
}
}
Старый код на Erlang подключают через external. Это же окно для библиотек, которых пока нет в Hex.
@external(erlang, "crypto", "strong_rand_bytes")
fn random_bytes(count: Int) -> BitArray
Тесты для обработчика не поднимают сеть: запрос собирают как структуру, ответ проверяют по статусу и телу. Это делает проверку дешёвой и быстрой, а интеграционные тесты остаются для проверки сборки целиком. Для фоновых задач используют те же акторы и супервизоры, что и в Erlang.
Сборка и деплой
Релиз собирается в самодостаточный каталог с Erlang-рантаймом и скриптом запуска. Дальше его кладут в образ или на сервер и запускают под systemd.
gleam export erlang-shipment
ls build/erlang-shipment
./build/erlang-shipment/entrypoint.sh run
Для JavaScript-цели достаточно gleam build —target javascript. Помните, что на JS нет процессов BEAM: модель конкурентности там другая, и код с акторами придётся переписать.
В образ кладут только каталог сборки и минимальный рантайм, поэтому итоговый слой получается небольшим. Конфигурацию передают переменными окружения, секреты — через файлы или менеджер секретов. Для systemd достаточно указать WorkingDirectory, ExecStart и User, а перезапуск доверить параметру Restart=on-failure. Проверьте, что версия рантайма на сервере совпадает с той, под которую собирали релиз.
Когда Gleam не подойдёт
- Нужны макросы и глубокая метапрограмма — смотрите Elixir.
- Требуется зрелая библиотека под редкий протокол — проверьте Hex заранее.
- Команда не готова к строгим типам и явным ошибкам.
- Проект живёт на другой виртуальной машине и BEAM не нужен.
Итог
Gleam — практичный способ получить статическую типизацию на BEAM без отказа от OTP и экосистемы Erlang. Начните с небольшого сервиса: пара модулей, тесты, релиз через shipment. Если типы и явные Result ускоряют работу команды, расширяйте; если борьба с компилятором съедает время, у вас останется работающий код на Erlang или Elixir.