Обновить

Я пишу бэкенд на Gleam. Вот что я понял за полгода

Уровень сложностиСредний
Время на прочтение8 мин
Охват и читатели12K
Всего голосов 33: ↑33 и ↓0+40
Комментарии50

Комментарии 50

Я думал GLEAM - это другое)

Специфический профсоюз внутри M$FT

Представляю, что выдаёт гугл тем, кто впервые пытается загуглить этот язык. Зато запоминается!

Спасибо за контент по BEAM, Erlang и Elixir.

Тоже имею 20+ лет опыта разработки и пару лет назад погрузился в Elixir. Давно меня так ничего не впечатляло. Удалось получить работу, удалось написать несколько фичей на нем в прод, но пришлось вернуться к Ruby и менеджменту. Надеюсь еще написать что-то на Elixir, а позже может быть и на Gleam.

Спасибо! Очень знакомая история — после 20 лет в IT думаешь, что тебя уже сложно чем-то удивить. А потом пробуешь ФП на BEAM — и вдруг снова чувствуешь себя джуном, который открывает что-то по-настоящему новое. Давно забытое ощущение.

Интересно, что вы пришли через Elixir. Я сознательно выбрал Gleam именно из-за статических типов — после Go хотелось строгости, но без когнитивной нагрузки Rust. А что в Elixir впечатлило больше всего — BEAM как платформа или сам язык?

Конечно и BEAM как платформа, а так же концепции заложенные в язык и тулинг. Иммутабельность, паттерн-матчинг, пайп-оператов, with, док-тесты, качество документации. Многое, всего уже не помню тк пришлось отойти от темы (сбили на взлете, сокращения).

Да, пайп-оператор и иммутабельность — это то, что меняет голову навсегда. Я теперь даже в Go-коде ловлю себя на том, что думаю пайпами.

Сочувствую насчёт сокращений. Но BEAM-мышление не теряется — когда будет возможность вернуться, порог входа будет совсем другой.

5 лет в нашем проекте пишем микросервисы на erlang. Довольны как слоны.

До этого были плюсы. Вспоминаем их как страшный сон.

Одно плохо, сложно быстро найти нового человека в команду.

Зачем он C#-разработчикам, у которых есть F#? С exhaustive pattern matching, pipe-operator, мутабельными переменными и computational expression, на которых красиво делаются и state machines, и ранний return. Всё это в итоге можно собрать в один бинарник как в Go.

F# — отличный язык, и для C#-разработчиков это действительно более естественный шаг в ФП — остаёшься на .NET, вся огромная и зрелая экосистема под рукой. Спорить с этим было бы странно.

Разница — в рантайме. F# работает на CLR, Gleam — на BEAM. Это разные модели конкурентности: лёгкие процессы с изоляцией ошибок, supervision trees, hot code reload. Для голосовых навыков, чатов, real-time систем это имеет значение. Для типичного бэкенда — нет, и F# будет практичнее.

А насчёт мутабельных переменных — я в статье честно написал, что их отсутствие в Gleam иногда бесит. F# тут прагматичнее.

В современных версиях Java есть все те же удобства, куча зрелых библиотек, а пайпы можно писать через optional или stream.

Может я чего-то недопонял из материала, но для меня как джависта серьезных причин присмотреться к этому языку не нашлось.

Справедливо. Если Java закрывает ваши задачи и экосистема устраивает — нет смысла менять ради синтаксического сахара.

Разница глубже, чем пайпы. Optional и Stream — это ФП-элементы поверх императивного языка. В Gleam иммутабельность и exhaustive pattern matching — не опция, а единственный способ писать код. Нет null, нет исключений, нет мутабельного состояния. Компилятор не даёт забыть ветку в case — вообще, ни одну. В Java sealed classes + switch expressions приближаются к этому, но не заставляют.

Тут мог бы подойти котлин, если хочется уйти от многословности java. Получить null-safety на уровне типов и exhaustive pattern matching через when+sealed (компилятор тоже не даст забыть ветку). Плюс он поощряет иммутабельность и функциональщину. Но несмотря на это, всё же является мультипарадигменным ЯП. Ещё очевидный плюс - большая экосистема вокруг языка. Но не факт, что она тут пригодится.
Если цель - писать чистый ФП-код и уйти от минусов jvm к минусам beam, то gleam действительно кажется хорошим вариантом, если исходить из статьи (не писал на нём).
А в elixir, кстати, скоро появится gradual typing, сейчас он уже есть в RC-ветке.

В BEAM большая ценность это сама платформа. Акторная модель, возможность запустить, к примеру, 300к параллельных процессов и сохранять отзывчивость. Построение кластерных приложений. Я не вижу конкурентов. За развитием Elixir слежу.

В мире JVM тоже все это есть - Akka (Pekko)

Есть, но только с виду. Ты не можешь запустить на одной машине 300к процессов, что бы они все отзывчиво работали и общались между собой. Ты не можешь так же легко создать кластерное приложение где акторы будут прозрачно общаться между нодами.

Akka это попытка сделать акторную модель на неакторной VM) А BEAM уже изначально построен на надёжной обслуживаемости параллельных процессов и акторы там природные - по-другому там не получится, да и незачем.

Все эти Pekko, Reaktor и тд - это как гонка по затаскиванию хаскеля и скалы во все GIL-языки, а затем и в Java, C#

NullPointerException пугает даже меня - не джависта. Одна из причин присмотреться к другим языкам

У JVM и BEAM коренные различия в работе в целом и в подходе к обслуживанию параллельных процессов в частности. BEAM изначально проектировался для обеспечения стабильной работы при обслуживании огромного количества параллельных соединений. К примеру, WS сервер однозначно выиграет и по стабильности и по soft-realtime и по экономичности(1GB RAM для JVM и 1GB для BEAM - это абсолютно разные возможности для реализации надёжных и стабильных сервисов).

Но правило "для каждой задачи свой молоток" никто не отменял)))

Можете развернуть мысль, в чем, как вы считаете, боль хаскеля ?

Говорю как человек с двадцатью годами императивного кода, не как эксперт по Haskell. Моя «боль» — это боль порога входа. Чтобы написать HTTP-сервер на Gleam, мне хватило выходных. Чтобы сделать то же на Haskell, мне сначала нужно понять, почему IO — это монада и зачем мне это знать.

Порог входа — не в синтаксисе, а в количестве концепций, которые нужно усвоить до того, как начнёшь писать что-то полезное. Монады, функторы, классы типов, ленивые вычисления по умолчанию, IO как монада. Каждая по отдельности понятна, но все вместе — это серьёзная нагрузка на мозг.

Уверен, что для тех, кто уже в Haskell, это не боль, а сила. Но я писал статью для таких же как я — Go/Java/C#-разработчиков, которые хотят попробовать ФП без огромных временных затрат.

Спасибо. А вот скажите как обстоят дела с логированием тут ? Можно встроить это в pipe ? И как вообще работать с IO ?

Логирование в Gleam можно условно разделить так:

Дебаг. Встроенное ключевое слово echo — вставляешь в любое место пайпа, оно пропускает значение через себя и печатает в stderr с указанием файла и строки:

[1, 2, 3]
|> list.map(fn(x) { x + 1 })
|> echo
|> list.map(fn(x) { x * 2 })

Билд-тул предупредит, если забудете убрать перед публикацией. Есть ещё io.debug — делает то же самое, но без метаданных.

Боевое логирование. Пакет logging — обёртка над Erlang logger. Уровни, форматирование, всё стандартно. Есть glimt — более продвинутый, со структурированным JSON-выводом и контекстами.

Мне вот интересен Lean 4. Это одновременно и язык программирования, и помощник для доказательств. Он помогает изучать математику. Если ты доказал теорему, и верифицировал доказательство в Lean, можно быть практически уверенным, что доказательство правильное.

Интересная ниша — язык как инструмент доказательства, а не только разработки. С Lean не работал. Ждем от вас статью про него))

IO является монадой потому, что ввод/вывод в haskell сделали на монадах. Почему нужно знать, что IO монада? Потому, что нужно уметь работать со значениями "упакованными" в монаду IO, а это умеет делать оператор bind -- >>= (еще это умеют делать unsafePerformIO и друзья, но unsafe в названии у них не просто так! И со значениями в монаде IO надо работать именно с помощью bind). Все просто!

Спасибо за объяснение! Вы как раз подтверждаете мою мысль — для вас это «всё просто», и я верю. Но в вашем объяснении уже есть «монада», «bind», «>>=», «unsafePerformIO» и «упаковка значений». Для обычного разработчика, который хочет напечатать строку в консоль, это пять новых концепций до первого результата. В Gleam это io.println("hello") — и готово.

Спасибо за краткое знакомство с Gleam. Буду рад дополнительным материалам по нему.

Спасибо! Постараюсь подготовить интересный материал.

Оператор |> в Gleam делает код читаемым слева направо, а не изнутри наружу

Крутецки выглядит. А можно передать собранную пайпу как лямбду аргументом куда-нибудь?

Сам пайп |> — это синтаксический сахар, а не значение, его нельзя передать куда-то как аргумент. Но можно собрать цепочку в анонимную функцию и передать её:

let transform = fn(x) {
  x
  |> string.trim
  |> string.uppercase
  |> string.append("!")
}

list.map(items, transform)

Я перестал понимать. Это во что развернётся?

Если…

raw_body
|> parse_request
|> handle_input
|> response.build
|> json.to_string

…разворачивается в json.to_string(response.build(handle_input(parse_request(raw_body)))), то…

x
  |> string.trim
  |> string.uppercase
  |> string.append("!")

…должно развернуться в string.append(string.uppercase(string.trim(x)), "!")?

Если да, то как-то упячно выглядит. По сравнению с традиционным:

x
  .trim()
  .uppercase()
  .append("!")

Я изначально подумал, что |> удобен, чтобы собирать конвейер из разных частей. Например, handle_input, response.build и json.to_string. А для набора операций обычной иммутабельной строки зачем? Где неявно передаётся this?

А вообще, меня эта тема зацепила тем, что я давно думаю, как сделать идеальные чейнинги (на JS или других, более функциональных, языках), где одним выражением можно результат первого filter разбить на два других filter, и с каждым сделать что-то своё, без промежуточных переменных. Когда говорят, что «jQuery устарел», в этом отношении это правда — он так не умеет )) А хотелось бы.

Да, разворачивается именно так. И вы правы — для строки x.trim().uppercase().append("!") читается лучше. Но в этом и суть: method chaining работает, пока все методы принадлежат одному объекту. Как только нужно протащить данные через функции из разных модулей — parse_request, потом handle_input, потом response.build — методы кончаются, а пайп продолжает работать.

В Gleam нет методов и нет this — функции принадлежат модулям, а не данным. Поэтому string.trim(x) вместо x.trim(). Пайп компенсирует отсутствие методов — и делает это универсально, потому что работает с любой функцией, а не только с методами одного типа.

Кстати, ваша идея про разветвление после filter — это по сути то, что в ФП делается через let. Не промежуточные переменные в императивном смысле, а именованные привязки:

let filtered = items |> list.filter(is_active)
let group_a = filtered |> list.filter(is_premium)
let group_b = filtered |> list.filter(fn(x) { !is_premium(x) })

Пайп не пытается заменить всё — он для линейных цепочек. Когда поток разветвляется — let и отдельные пайпы.

Ясно, спасибо.

Кстати, ваша идея про разветвление после filter — это по сути то, что в ФП делается через let. Не промежуточные переменные в императивном смысле, а именованные привязки

Это меня как раз и не устраивает. У вас в примере group_a = filtered |> list.filter(is_premium). Я полагаю, это только для примера, в жизни, понятное дело, напишут не group_a, а premium_group, и нарушится DRY: is_premium УЖЕ описывает ситуацию. Если всерьёз рассмотреть использование имён типа a, это не способ избежать нарушений DRY, это способ замаскировать нарушение. Единственный способ избежать нарушений DRY — вообще избегать всего поименованнного, поэтому я и думаю, как такие вещи писать одним выражением. Хотя бы запись чтоб была исключающая противоречия, уж потом можно подумать про атомарность транзакции и т.п.

Да для примера. Но мне кажется, тут не нарушение DRY, а два разных уровня абстракции. is_premium — это предикат. premium_group — это результат применения предиката к конкретному набору. Они не дублируют друг друга — они на разных этажах.

А потом в коде попадается const buttons = $('.card');, и что должен подумать разработчик? (Как на том детском кубике, где буква «Э» сопровождается словом «Свинья» и изображением утки). Когда человек вместо переменных пишет просто $('.card') или $('button'), это заставляет следить за разметкой. Если нам действительно нужны мутанты, они должны быть явно названы .card-button. Именно в этом прелесть $(), а не в том, что он когда-то чего-то там полифилил (как постоянно приходится читать от людей, не постигших дзен чейнинга).

Проблема в том, как чисто технически исключить переменные, если, например, мы сначала из всех элементов отбираем карточки, а потом карточки с картинками делаем меньше, а карточки без картинок, но с заголовками — больше. И, конечно, одним запросом.

Тут я пас - фронтенд не моя специализация. Но идея у вас звучит как интересная тема для отдельной статьи.

  1. Думается, что на бэкенде и в других местах ситуация аналогичная. Я просто взял пример, который мне ближе.

  2. Подпишусь на вас, на случай, если вы напишете отдельную статью. Для меня в прикладном смысле это очень интересная тема.

Rust при прочих равных лучше подходит, я считаю. Все перечисленное он лучше умеет. При необходимости можно переключаться на императивный стиль. Экосистема лучше. ИИ агенты с растом дружат лучше, чем с экзотикой. Когнитивная нагрузка выше, если надо работать с чужим кодом. Если код контролировать, то все ок.

Удивлен, что при таком богатом опыте с другими языками, вы еще пишете на Go. Я от него испытал отвращение - все сделано максимально криво и првоцирует писать плохой код.

Про Rust — согласен, что он мощнее как язык. Но для моей задачи (голосовой навык, FSM, Redis, конкурентные сессии) его сила избыточна, а BEAM под капотом Gleam даёт модель конкурентности, которой в Rust нет из коробки — лёгкие процессы, изоляция ошибок, supervision trees. Это не про синтаксис, а про рантайм.

Про Go — я в основном занимаюсь архитектурой, кода пишу мало. Go в нашем стеке — не мой личный выбор, а стандарт, который выбирает компания. И его выбирают именно за то, за что вы его критикуете — он простой, предсказуемый, любой новый разработчик входит в проект за короткий срок. Для команд в 10-50 человек это перевешивает элегантность. Отвращение к Go я понимаю, сам в статье написал про бесконечные «if err != nil» — но архитектурные решения принимаются не по ощущениям одного человека, а по стоимости поддержки для всей команды.

Как будто бы Scala хорошо подошла. Не рассматривали её в качестве альтернативы?

Более зрелые тулы, функциональный подход, грин треды, авторы где если хотите

Scala не рассматривал, честно. Она мощная, но для side-проекта мне был важен минимальный порог входа. Gleam можно выучить за выходные — у Scala порог существенно выше, особенно если до этого не был в JVM-мире.

Мне кажется или у автора все комментарии написаны ИИ

Ага, и твой )

Так точно, товарищ майор))

Причем тут вообще товарищ майор? Как это связано? С кем я общаюсь, openclaw?

Переоцениваете вы меня. Мне до openclaw далеко. Я только-только учусь структурировать мысли - решил по этому поводу статью написать. А "товарищ майор" - это мем, не принимайте близко к сердцу :)

Если бы комментарии писал ИИ - он бы не признался, что у него есть сложности с другими языками, типа Haskell. У него слишком высоки репутационные риски. Кто им будет пользоваться - после таких заявлений?

А почему вы в этом комментарии сменили '—' на '-' внезапно?

Потому что на длинные тире закончились токены :)

Теория мертвого интернета подтверждается )

Жаль, что создатели Gleam по пути к статической типизации выкинули действительно интересные фишки Erlang:

  • паттерн матчинг в аргументах функции (перезагрузка функций)

  • send/! и receive

Последнее особенно обидно, так как возможности Beam как раз заточены на интеркоммуникацию легковесных функций. Конечно, решается, но ценой перехода на программирование на эрланге.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации