
У меня двадцать лет в IT. Большую часть этого времени я проектировал и эксплуатировал инфраструктуру на PostgreSQL. Сейчас работаю архитектором: Go, Python, Postgres, Redis, ClickHouse, мониторинг на десятки тысяч баз. До этого писал на Ruby, пробовал Rust. Классический бэкенд-инженер со всеми вытекающими привычками: императивный код, мутабельное состояние, постоянные if err != nil { return err }.
А потом я начал писать бэкенд на Gleam — молодом функциональном языке на BEAM (Erlang VM), который появился в стабильной версии только в 2024 году. Навык ещё в разработке, но бэкенд уже работает, и я не жалею. Путь был... познавательным.
Эта статья — не туториал и не рекламный буклет. Это честный рассказ о том, почему я выбрал Gleam, какие шишки набил, что мне понравилось настолько, что я не хочу возвращаться, и что до сих пор бесит.
Что я делаю?
Разрабатываю голосовую игру для Яндекс Алисы. Ребёнок управляет кафе: принимает заказы, готовит блюда, обслуживает посетителей. Звучит просто, но под капотом:
HTTP-сервер, обрабатывающий запросы от платформы Алисы (JSON-протокол со своей спецификой)
Конечный автомат (FSM) с десятками состояний и переходов между сценами
Хранение состояния сессий в Redis (Алиса — stateless, состояние нужно передавать или хранить самому)
HMAC-SHA256 подпись состояния для защиты от подделки
Админ-панель на React для управления диалоговыми сценариями через API
Озвучка через ElevenLabs, звуковые эффекты, SSML-разметка
Стек: Gleam + Wisp (HTTP) + Mist (сервер) + Valkyrie (Redis) + Nginx + systemd + Ubuntu. Деплой — обычный Linux-сервер, без Docker и Kubernetes.
Почему Gleam, а не [ваш любимый язык]
Почему не Go
Go — основной язык в моём стеке, на нём написаны системы, которые я проектирую и поддерживаю. Для инфраструктурных сервисов он прекрасен: быстрая компиляция, горутины, один бинарник. Но для моделирования предметной области Go — страдание.
В Go нет алгебраических типов данных. Нет pattern matching. Обработка ошибок — бесконечные if err != nil. Когда ты моделируешь конечный автомат с двадцатью состояниями, где каждый переход должен быть валидным, Go превращается в минное поле: ты постоянно боишься забыть проверку, пропустить кейс, оставить невалидное состояние.
Почему не Rust
Rust я изучал перед Gleam и даже написал на нём прототип. Rust великолепен для системного программирования, но для HTTP-бэкенда с Redis — избыточен. Borrow checker решает проблемы, которых у меня нет: я не управляю памятью вручную, не пишу драйверы. А вот когнитивная нагрузка от сложности языка — это цена, которую я плачу за ненужные мне гарантии.
Почему не Elixir
Elixir — ближайший родственник Gleam, тоже на BEAM. Отличный язык. Но: динамическая типизация. Для небольшого-проекта, где я не могу позволить себе роскошь тестового отдела и CI с тысячей тестов, статические типы — это мой страховой полис. Gleam ловит целые классы ошибок на этапе компиляции, которые в Elixir всплывают только в runtime.
Почему Gleam
Gleam попал в пересечение всех моих требований:
Статическая типизация с алгебраическими типами и pattern matching — для моделирования FSM(конечного автомата).
BEAM под капотом — конкурентность, отказоустойчивость, горячая перезагрузка (теоретически).
Простой синтаксис — после Rust - это как задержать дыхание на пару минут и потом выдохнуть. Все предельно просто, никаких макросов, минимум магии.
Result-типы вместо исключений — каждая ошибка явная, каждый путь обработан.
JavaScript-интероп — для тех случаев, когда нужной библиотеки на Gleam нет (а таких случаев много).
Конечный автомат на типах - главная победа
Конечный автомат — сердце голосового навыка. У игры есть сцены (главное меню, кухня, зал, мини-игра), и в каждой сцене — свои допустимые переходы. Игрок не может «готовить», находясь в главном меню, и не может «принять заказ» на кухне.
В Go или Python это обычно делается через строки или enum-ы с runtime-проверками:
// Go — runtime error, если забыл проверку if state == "kitchen" && action == "take_order" { return errors.New("invalid action for kitchen") }
В Gleam это моделируется на уровне типов:
// Gleam — невалидный переход не компилируется pub type Scene { MainMenu Kitchen(KitchenState) Hall(HallState) MiniGame(MiniGameType) } pub type KitchenState { Idle Cooking(dish: Dish, time_left: Int) Done(dish: Dish) } pub fn handle_kitchen(state: KitchenState, input: UserInput) -> Result(Scene, UserError) { case state, input { Idle, Cook(dish) -> Ok(Kitchen(Cooking(dish, dish.cook_time))) Cooking(_, ), _ -> Error(StillCooking) Done(dish), Serve -> Ok(Hall(Serving(dish))) _,_ -> Error(InvalidAction) } }
Красота в том, что компилятор заставляет меня обработать все комбинации состояний и действий. Если я добавлю новую сцену и забуду написать обработчик — код не скомпилируется. В Go или Python это была бы ошибка, которая проявилась бы только, когда реальный пользователь попал бы в необработанную ветку.
Typed Errors: почему UserError лучше, чем panic
В начале проекта я обрабатывал ошибки как в Go — возвращал строки. Потом рефакторнул на типизированные ошибки, и это изменило всё.
pub type UserError { SessionExpired InvalidState(details: String) RedisConnectionFailed(reason: String) SceneNotFound(scene_id: String) HmacVerificationFailed }
Теперь каждая функция явно декларирует, что может пойти не так:
pub fn load_session(session_id: String) -> Result(Session, UserError) { use raw <- result.try( redis.get(session_id) |> result.map_error(fn(e) { RedisConnectionFailed(e.message) }) ) use session <- result.try( decode_session(raw) |> result.map_error(fn(_) { InvalidState("corrupted session data") }) ) verify_hmac(session) |> result.map_error(fn(_) { HmacVerificationFailed }) }
Оператор use в Gleam — это то, что делает цепочки Result-ов читаемыми. Без него код был бы лестницей из case ... { Ok() -> ... Error() -> ... }. С ним — линейный поток, где каждый шаг может упасть, и ты точно знаешь, как именно.
Экосистема: где больно
Не буду врать: экосистема Gleam — это одновременно и самая сильная проблема, и причина, по которой ты можешь стать контрибьютором чего-то важного.
Redis: путь от radish к valkyrie
Первый клиент Redis, который я попробовал — radish. Работал, но с ограничениями. Потом перешёл на valkyrie — более зрелый, лучше API. Но даже valkyrie не покрывает все команды Redis. Несколько раз пришлось лезть в исходники и дописывать.
Для сравнения: в Go я бы взял go-redis и не думал. В Gleam выбор Redis-клиента — это исследовательский проект на пару вечеров.
HTTP: Wisp и его особенности
Wisp — HTTP-фреймворк для Gleam. Работает поверх Mist (HTTP-сервер на BEAM). В целом — приятный, минималистичный. Но есть неочевидный момент: Wisp использует String-based API, а не StringTree (StringBuilder). Я потратил час на отладку, потому что пытался передать StringTree туда, где ожидался String. Компилятор подсказал, но не сразу было ясно, почему типы не совпадают.
Библиотеки: чего не хватает
На момент написания статьи мне не хватало:
Полноценного HMAC-SHA256 (пришлось делать через Erlang FFI).
Продвинутой работы с JSON (gleam_json базовый, для сложных вложенных структур неудобен).
Хорошего логгера с уровнями и структурированным выводом.
ORM или хотя бы query builder.
Это цена раннего использования нового языка. Но это и возможность: каждая библиотека, которую ты напишешь, получает очень высокий шанс стать БИБЛИОТЕКОЙ в экосистеме.
Что мне понравилось настолько, что я не хочу возвращаться к другим языкам
Exhaustive pattern matching
Когда я добавляю новый вариант в тип — например, новую сцену в игре — компилятор показывает все места, где я забыл его обработать. Не один, не два — все. В Go я бы узнал об этом от пользователя в production.
Pipe-оператор
Оператор |> в Gleam делает код читаемым слева направо, а не изнутри наружу:
// Вместо этого: json.to_string(response.build(handle_input(parse_request(raw_body)))) // Пишешь это: raw_body |> parse_request |> handle_input |> response.build |> json.to_string
После pipe-оператора возвращаться к вложенным вызовам физически неприятно.
Иммутабельность по умолчанию
Всё иммутабельно. Нет переменных, которые кто-то может изменить из другого потока. Нет race conditions на уровне данных. Для бэкенда, обрабатывающего конкурентные запросы — это не просто удобство, это архитектурная гарантия.
Сообщения об ошибках компилятора
Gleam унаследовал от Elm-традиции человечные сообщения об ошибках. Когда типы не совпадают, компилятор не просто говорит «type mismatch» — он объясняет, что ожидалось, что получено, и часто подсказывает, как исправить. После gcc и даже rustc — это глоток свежего воздуха.
Что бесит
Нет мутабельных переменных совсем. Иногда хочется просто инкрементировать счётчик. Мне сложно игнорировать свои 20 лет программирования с мутабельными переменными. В Gleam для этого нужен рекурсивный вызов или fold. Идеологически чисто, практически — иногда утомляет.
Нет раннего return. В Go я пишу guard clause — проверил условие, вернулся. В Gleam каждый путь должен быть частью выражения. Для сложных валидаций это удлиняет код.
Маленькое сообщество. Если ты застрял — Stack Overflow не поможет. Discord Gleam — основной канал поддержки, и он отзывчивый, но это не масштаб Go или Python.
Отсутствие IDE-поддержки уровня GoLand/IntelliJ. Gleam LSP работает, но рефакторинг, навигация, автодополнение — всё на уровне «достаточно, но не более».
Документация библиотек. Hexdocs есть, но многие пакеты документированы по принципу «типы говорят сами за себя». Иногда говорят. Иногда — молчат.
Деплой: скучно и надежно
У меня простой проект. Поэтому: никакого Docker, никакого Kubernetes.
Вот весь деплой:
gleam export erlang-shipment — собирает релиз
scp на сервер
systemd unit для запуска и рестарта
Nginx как reverse proxy с SSL
BEAM-релиз — это самодостаточная директория с Erlang runtime и скомпилированным кодом. Работает как обычный процесс. Потребление памяти — около 30–50 МБ. Для side-проекта на VPS за 500 рублей в месяц — более чем достаточно.
Стоило ли оно того?
Да. Но с оговорками.
Стоило, потому что: я перестроил своё мышление. После двадцати лет императивного кода я теперь думаю типами и паттернами, а не циклами и мутациями. Это сделало меня лучшим инженером даже в Go — я стал чаще использовать интерфейсы как discriminated unions, строже обрабатывать ошибки, избегать мутабельного состояния.
Стоило, потому что: Gleam заставляет проектировать перед кодированием. Когда у тебя нет null, нет исключений, нет мута>ций — ты вынужден продумать все пути данных заранее. Это медленнее на старте и быстрее на дистанции.
С оговоркой: для production-системы с SLA и on-call я бы пока выбрал Go или Elixir. Экосистема Gleam не готова для «взрослого» production, где нужны десятки интеграций из коробки. Но для side-проекта, pet-проекта, микросервиса с узким скоупом — Gleam идеален.
Кому стоит попробовать Gleam
Go/Java/C#-разработчикам, которые хотят попробовать функциональное программирование без боли Haskell. Gleam — самый мягкий вход в мир типизированного функционального программирования.
Elixir-разработчикам, которым не хватает статических типов. Gleam компилируется в Erlang и может использовать любые Erlang/Elixir-библиотеки.
Тем, кто пишет конечные автоматы, парсеры, обработчики протоколов — задачи, где важна корректность и полнота обработки всех случаев.
Тем, кто устал от if err != nil и хочет попробовать мир, где обработка ошибок — не ритуал, а часть дизайна.
Вместо заключения
Gleam — не серебряная пуля. Это молодой язык с маленькой экосистемой и амбициозными идеями. Но он решает реальную проблему: даёт статическую типизацию и функциональную чистоту без порога входа Haskell и без когнитивной нагрузки Rust.
Если вы двадцать лет писали императивный код и чувствуете, что стагнируете — попробуйте написать что-нибудь на Gleam. Необязательно целый бэкенд. Начните с Exercism, потом — CLI-утилита, потом — маленький API. Ваш мозг скажет спасибо.
А если напишете библиотеку — вы автоматически станете одним из ведущих контрибьюторов экосистемы. Нас пока мало. Заходите.
P.S. Если интересно, в следующей статье покажу подробнее архитектуру конечного автомата на типах Gleam — как моделировать диалоговые сценарии так, чтобы невалидные переходы были невозможны на уровне компиляции.
