В разработке на Gleam одна и та же бизнес-логика часто повторяется под разные способы получения данных: синхронный, с ошибками, асинхронный. Допустим, есть функция fetch, которая по строковому ключу возвращает строку. Логика превращает список ключей в верхний регистр и считает длину каждого значения.
Поначалу всё просто — simple_func работает с fn(String) -> String. Потом бизнес просит обработку отсутствующих значений: fetch возвращает Result(String, Nil), и пишется fallible_func. Затем запрос на асинхронность — async_func с Promise(String).
Код каждый раз почти одинаковый — меняется только тип fetch и обёртка вокруг результата. А когда нужно одновременно и ошибки, и асинхронность (Promise(Result(...))), придётся писать четвёртую версию. Дублирование растёт.
Решение — continuations. В Gleam это тип Continuation(t, a) = fn(fn(a) -> t) -> t. Он представляет собой «я не знаю, как именно ты получишь значение, но когда получишь — сделай вот это». Обобщённый параметр t скрывает детали эффекта (синхронность, ошибку, асинхронность).
Пишется одна функция task, которая принимает fetch возвращающий Continuation(t, String). Логика внутри та же самая: преобразование ключа, вызов fetch, вычисление длины. Выход — Continuation(t, List(Int)). Всё, код бизнес-логики определён один раз.
Теперь нужны «исполнители» (runners). Каждый runner конкретизирует t и задаёт, как именно работает fetch.
run_simple:fetchвсегда возвращает"yes", финальный callback —fn(x) { x }. На выходеList(Int).run_fallible:fetchищет ключ в словаре, при ошибке возвращаетError(Nil). Callback —Ok. Итог —Result(List(Int), Nil).run_async:fetchждёт 100 мс, потом вызываетthen("slow"). Callback —promise.resolve. Выход —Promise(List(Int)).
Так одна и та же task работает с любым эффектом — нужно только подставить свой runner. Это и есть абстракция эффектов с помощью continuations. Разделение логики и эффектов. Автор параллельно строит проект EYG — эксперимент по улучшению языков и инструментов (новости о нём в рассылке).