У меня двадцать лет в 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 — как моделировать диалоговые сценарии так, чтобы невалидные переходы были невозможны на уровне компиляции.

Only registered users can participate in poll. Log in, please.
Ваш опыт с функциональным программированием
18.37%Пишу на ФП-языке в продакшене9
18.37%Пробовал для pet-проектов9
36.73%Использую элементы ФП в императивных языках (map/filter/reduce и т.д.)18
16.33%Давно присматриваюсь, но не начинал8
10.2%Не вижу смысла, и так всё работает5
49 users voted. 14 users abstained.
Only registered users can participate in poll. Log in, please.
О чём интересно почитать дальше?
56.67%Архитектура FSM на типах Gleam — как сделать невалидные состояния невозможными17
66.67%Экосистема Gleam подробнее — библиотеки, грабли, Erlang FFI20
26.67%Деплой BEAM-приложения без Docker — systemd, nginx, мониторинг8
36.67%Сравнение с Elixir — зачем статические типы на BEAM11
30 users voted. 12 users abstained.