Привет!
Подумал я тут рассказать вам о том, как в JavaScript с помощью библиотеки Fluture можно создавать и использовать ленивые функции. Это будет краткий обзор на то, как создавать функции, как обрабатывать ошибки и чуть-чуть про параллелизм. Функциональным программированием мозги парить не буду! Обещаю!
Fluture
Fluture — библиотека, разработанная разработчиком Aldwin Vlasblom, реализующая Future. Future — альтернатива Promise, имеющая куда более мощный API, позволяющий реализовать отмену выполнения (cancellation), безопасную "рекурсию", "безошибочное" выполнение (используя Either) и ещё маленькую тележку крутых возможностей.
Думаю, стоит рассказать вам про методы (монады), которые я буду использовать в примерах ниже
.of(Any)
— создает Future из переданного значения.map(Function)
— нет, это неArray.map
, это функция трансформации, аналогичнаяPromise.then
.chainRej(Function)
— аналогичноPromise.catch
ловит ошибку.fork(Function, Function)
— запускает выполнение Future
Создание ленивой функции
Для себя я выделил два основных подхода к созданию ленивых функций в Fluture. Первый подход заключается в том, что мы создаем функцию, которая принимает исходные данные и возвращает готовую к выполнению Future. Второй подход заключается в том, что мы создаем Future со всеми описанными трансформациями, а затем передаем ей данные.
Непонятно? Давайте на примере! Есть у нас вот такая функция
const multiply10 = x => x * 10;
Теперь сделаем её ленивой, используя первый подход
const multiply10 = x => x * 10;
const lazyMultiply10 = (x) =>
Future
.of(x) // Создаем Future из значения
.map(multiply10); // Теперь наша функция тут
lazyMultiply10(2).fork(console.error, console.log);
// -> 20
Слишком громоздко, не правда ли? Попробуем записать более лаконично, используя второй подход.
const multiply10 = x => x * 10;
const lazyMultiply10 = Future.map(multiply10);
const value = Future.of(2); // Оборачиваем наше значение в Future
lazyMultiply10(value).fork(console.error, console.log);
// -> 20
Уже лучше, но все еще громоздко. Надо компактнее!
const lazyMultiply10 = Future.map(x => x * 10);
lazyMultiply10(Future.of(2)).fork(console.error, console.log);
// -> 20
На самом деле, эти подходы не являются взаимоисключающими и могут быть использоваться вместе.
const lazyMultiply10 = Future.map(x => x * 10);
const someCalculation = a =>
Future
.of(a)
.map(v => v + 1)
.chain(v => lazyMultiply10(Future.of(v));
someCalculation(10).fork(console.error, console.log);
// -> 110
Обработка ошибок
Обработка ошибок в Future практически не отличается от обработки ошибов в Promise. Давайте вспомним представим функцию, которая делает запрос к стороннему, не очень стабильному, API.
const requestToUnstableAPI = query =>
request({
method: 'get',
uri: `http://unstable-site.com/?${query}`
})
.then(res => res.data.value)
.catch(errorHandler);
Та же функция, но обернутая в Future
const lazyRequestToUnstableAPI = query =>
Future
.tryP(() => request({
method: 'get',
uri: `http://unstable-site.com/?${query}`
}))
.map(v => v.data.value)
.chainRej(err => Future.of(errorHandler(err));
На самом деле, обработку ошибок можно сделать более гибкой. Для этого нам понадобится структура Either, а это малость выходит за рамки моего обзора.
Параллелизм
Для работы с параллелизмом в Future реализованы два метода race(Futures[])
(аналогичен Promise.race
), parallel(n, Futures[])
и both(Future, Future)
, но он является частным случаем parallel
.
Метод parallel
принимает два аргумента, количество параллельно выполняемых Future и массив с Future. Чтобы сделать поведение parallel
таким же как метод Promise.all
, нужно количество выполняемых установить как Infinity
.
Тут тоже без примеров не обойдемся
const requestF = o => Future.tryP(() => request(o));
const parallel1 = Future.parallel(1);
const lazyReqs = parallel1(
[
'http://site.com',
'http://another-site.com',
'http://one-more-site.com',
]
.map(requestF)
);
lazyReqs.fork(console.error, console.log);
// -> [Result1, Result2, Result3]
Совместимость с Promise
В JavaScript от Promise никуда не деться, да и вряд ли кто-то будет рад, если ваш метод будет возвращать какую-то непонятную Future. Для этого у Future есть метод .promise()
, который, запустит выполнение Future и обернет её в Promise.
Future
.of(10)
.promise();
// -> Promise{value=10}
Ссылки
- Репозиторий библиотеки Fluture
- Статья на Medium от Aldwin Vlasblom "Broken Promises"
- Спецификация fantasy-land
Вот, пожалуй, и все, что я вам хотел рассказать. Если тема интересна, дайте знать, расскажу подробнее про обработку ошибок. И да, сильно меня не ругайте, это мой первый пост на Хабре.
Комментарии (41)
vintage
12.01.2019 19:45Это же стримы типа RxJS.
ArturAralin
12.01.2019 19:59Stream из RxJS не то, чем является Future. Просто они похожи способом трансформации данных
vintage
12.01.2019 21:22Ну а в чём отличие?
ArturAralin
12.01.2019 21:40Я не большой специалист в RxJS, т.к. в основном занимаюсь back-end разработкой, но грубо говоря RxJS это про управление событиями и потоками данных, а Future про отложенные вычисления
napa3um
12.01.2019 23:12Не очень понятно, как «оправдывает» вас бэкенд-разработка, rxJS прекрасно работает и на бэкенде. И да, Future вполне себе реализовывает подмножество возможностей rxJS, и если в проекте уже внедрён rxJS, то нет никаких причин для внедрения ещё и Fluture. Всё, что можно описать на Fluture, опишется на rxJS примерно таким же количеством кода.
ArturAralin
13.01.2019 00:24Не очень понятно, как «оправдывает» вас бэкенд-разработка, rxJS прекрасно работает и на бэкенде
Я нигде и не говорил, что RxJS не работает на back-end.
Что касается "оправдания", то на моей практике задачи требующие FRP это редкость, а там, где потенциально этот подход можно использовать, было не целесообразно подключать библиотеку для реализации FRP
Future вполне себе реализовывает подмножество возможностей rxJS
Каждый инструмент имеет свое предназначение. Так-то и ломом можно консервные банки открывать
napa3um
13.01.2019 00:39-21) Вам явно не стоит хвастаться своей практикой.
2) Неповоротливым монолитным ломом является как раз Fluture, и если в проекте не нужно бОльших возможностей, бандл с rxJS получится меньше, при этом если нужно — то всё уже готово. А код семантически получится эквивалентный Флутеру, никакой дополнительной когнитивной нагрузки rxJS от вас не потребует (если вам от неё нужно только то, что нужно от Флутера).
3) Не воспринимайте вопросы или замечания собеседников о библиотеке как претензии к вам лично или вашему незнанию альтернатив. Или вы как-то относитесь к разработке этой библиотеки? (Тогда тем более стОит быть чуть потерпимее.)ArturAralin
13.01.2019 00:51-11) Я, пожалуй, сам решу делать мне это или нет
2) Кажется, вы судите только из специфики своей работы и инструментов, которые вы используете. Думаю вам стоит пойти в репозиторий обоих инструментов (RxJS и Fluture) и почитать об их предназначение и особенностях
3) <не совсем понял, что вы тут имели в виду>napa3um
13.01.2019 00:581) Я лишь даю вам дополнительную информацию для принятия вами самостоятельного решения.
2) rxJS использую регулярно, ещё с версии 3, Флатер пощупал тоже, и для этого мне таки пришлось сходить в его репозиторий и почитать документацию. И размер бандла тоже не телепатически выяснял, конечно.
3) Ничего страшного.
nsinreal
13.01.2019 01:06Вам явно не стоит хвастаться своей практикой.
Вы так говорите, как будто у FRP область применения шире, чем использование в академических статьях, pet-projectах и едких комментариях на хабре.
Если без шуток, то FRP преследует злой рок: даже там где он применяется, от него начинают потихоньку избавляться.napa3um
13.01.2019 01:11Если без шуток, то Observables из Rx проталкиваются в стандарт ES (Микрософт, автор Rx, вхож в комитет по стандартизации). Если вам не нужен FRP — ничего страшного, не нужно пытаться убеждать себя, что и другим оно не нужно.
nsinreal
13.01.2019 01:58Если вам не нужен FRP
Не то, чтобы мне не нужно было FRP. Мне не нужны текущие реализации FRP, потому что я не видел реализаций, совместимых с концепцией «понятно где и почему упало». К примеру, у промисов такая же проблема была долгое время, но в меньших масштабах. (Fluture тоже будет иметь такие же проблемы)
Помимо всего прочего, FRP выглядит просто на словах, но имеет дохрена тонкостей, которые раскрываются с каждым днем использования (сужу по rxjs). Мне кажется, что порог входа слишком высок, а область применения слишком мала.
не нужно пытаться убеждать себя, что и другим оно не нужно.
О, почти классика. Сначала вы обесценили опыт человека. Потом на едкое возражение «узкая область применения, чтобы это обесценивало чей-то опыт» вы выставляете виноватым опоннента в том, что он обесценивает FRP. Т.е. «все кто не используют FRP — дураки, а если кто не согласен, то он дурак». Прекрасная петля, почти как жопа Хэнка.
nsinreal
13.01.2019 02:07А код семантически получится эквивалентный Флутеру, никакой дополнительной когнитивной нагрузки rxJS от вас не потребует (если вам от неё нужно только то, что нужно от Флутера).
Это не так. Код, который продьюсит данные — он пожалуй, действительно, получится тем же самым. Но код, который использует данные — он получится совершенно другим.
В отличии от Promise/Future, Stream может содержать множество значений, даже бесконечность. И либо вы пишите код, который делает бесполезную работу (чтобы соблюсти семантику); либо вы нарушаете семантику и рискуете в будущем получить неработоспособный код.napa3um
13.01.2019 15:45-1Используемый код получит Observable, который в случае возврата лишь одного значения эквивалентен Promise/Future. Не пишите чепухи, Observable — это обобщение любых асинхронных источников данных. Если хотите защититься от утечек и гарантировать запуск обработчика лишь один раз — примените оператор first (хоть прям по месту использования обсервабла).
nsinreal
13.01.2019 19:02+1примените оператор first (хоть прям по месту использования обсервабла).
Я об этом писал: «либо вы нарушаете семантику и рискуете в будущем получить неработоспособный код».
Для примера: иногда люди реализовывают для улучшения отзывчивости UI эмит закешированных данных + эмит настоящих данных с сервера. В таком случае ваш код с first будет работать некорректно. Особенно красиво это смотрится в развертке времени: сначал код с first, потом улучшение отзывчивости — кто виноват в неработспособности системы?
Используемый код получит Observable, который в случае возврата лишь одного значения эквивалентен Promise/Future. Не пишите чепухи, Observable — это обобщение любых асинхронных источников данных
В случае настоящего Observable вам резко становится интересно:
— а какая разница между switchMap и flatMap?
— а зачем нужны debounce и throttle?
— зачем в rxjs имеется 19 filtering-операторов, 23 transformation-операторов и 12 combination-операторов и что будет, если я не знаю каждый?
— что такое hot и cold Observables и как превратить hot в cold?napa3um
13.01.2019 19:43-1Да что за бред-то? В вашем примере сломается и Фьюча, а вот Обсервабл как раз легко расширить на такой в ариант. Если изначально источник данных был промисовый, то Обсервабл не родит ничего больше одного сигнала, а в случае «настоящего» Обсервабла если вам стало интересно, то, значит, уже и задача поменялась и перестала укладываться в семантику Промиса/Фьючи. Для использования Обсервабла в качестве Промиса/Фьючи никаких особых знаний о Скедулере иметь не нужно.
Из диалога с вами ретируюсь, дальше без меня.nsinreal
13.01.2019 19:52Из диалога с вами ретируюсь, дальше без меня.
Вы слились, но я все же напишу, почему натягивать на Observable семантику Promise/Future — плохая идея. Остальным может быть полезно.
В вашем примере сломается и Фьюча, а вот Обсервабл как раз легко расширить на такой в ариант
Есть разница между «код, который не компилируется» и «код, который притворяется, что он работает».
Для использования Обсервабла в качестве Промиса/Фьючи никаких особых знаний о Скедулере иметь не нужно
А я ведь именно про скедулер вообще ничего не говорил. Я упомянул про hot/cold observables в контексте того, что Angular2+ явно проиллюстрировал проблему того, что никто не ожидал hot observables от HttpClient и не умел с ними работать.
vintage
13.01.2019 19:48Можно подумать для Fluture не нужны все эти debounce, throttle, 19 filtering, 23 transformation, 12 combination операторов.
nsinreal
13.01.2019 19:54Не нужны большинство из них.
Ну или вам придется просветить меня: зачем debounce/throttle в семантике одного отложенного значения? Они же заточены под «множество» значенийvintage
13.01.2019 22:46Затем чтобы не приходилось все места очередного запуска этого «одного отложенного вычисления» заворачивать вручную в debounce/throttle.
nsinreal
13.01.2019 22:55Омг, вам говорят про только одно значение, а вы говорите — а вот там извне будет много значений. Не, не будет. Потому что инструмент выбран соответственно задаче и нет соблаза изменить задачу под инструмент.
vintage
13.01.2019 23:50Что за мифическая задача такая, где никогда не надо перезагружать данные?
nsinreal
14.01.2019 00:52- Обновлений практически нет — пример: всякие редакторы (редактор слайдов, графиков и т.д.) — необходимые ресурсы загружаются при инициализации; Перезагрузка не нужна.
- Обновления происходят редко — пример: многие новостные порталы. Обновление делегируется браузеру (F5)
- Предполагается короткое время жизни UI — пример: данные подгружаются в попапе, который будет закрыт через 10-30 секунд (попап сохранения в Google Drive). Перезагрузка не нужна.
- Загрузка происходит слишком долго — пример: генерация контента на лету. Нужно не debounce делать, а показывать, что что-то грузится очень долго и блочить возможность повторных попыток. Натягивать это на стрим — можно, но не нужно
- Общая несовместимость концепта с перезагрузкой данных — опять же, какой угодно редактор, который не предполагает shared editing.
Вы знаете, существует потрясно много задач, в которых не нужен debounce.
И блин, если вам нужны обновления, то вместо pull семантики многие используют push-семантику, где не нужен debouncevintage
14.01.2019 09:43всякие редакторы (редактор слайдов, графиков и т.д.)
Если не троттлить ввод пользователя редактор будет тупить на чуть более сложном графике.
Обновления происходят редко
Не важно редко или часто. Важно, что происходят.
многие новостные порталы.
Блок "последние новости", "активные обсуждения" и тп.
попап сохранения в Google Drive
Это часть приложения, где очень много рилтайм данных. Один попап со статическим текстом погоды не делает.
Нужно не debounce делать, а показывать, что что-то грузится очень долго и блочить возможность повторных попыток. Натягивать это на стрим — можно, но не нужно
Мы так делали — было норм. Быстро отдаём фейковые заглушки и спокойно ждём загрузки реальных данных.
какой угодно редактор, который не предполагает shared editing
Таких почти не осталось.
существует потрясно много задач, в которых не нужен debounce.
И нет ни одной, где его поддержка была бы лишней. Вы что доказать-то хотите? Что использовать для разных кейсов 2 разных не совместимых апи лучше, чем один обобщённый?
вместо pull семантики многие используют push-семантику
- Ну и дураки.
- Rx, Promise, Fluture — это как раз push семантика.
ArturAralin
13.01.2019 20:13+1vintage, не нужны, т.к. все эти действия выходят за пределы ответственности Future. Для фильтраций, комбинаций и трансформаций используете
.map
или.chain
и используете те реализации функций, которые вам надо (ну или используйте что-то вроде Ramda или Sanctuary)vintage
13.01.2019 22:50Так и в стримах в качестве пайпов можно использовать любые реализации. Но они хотя бы не засовывают голову в песок, мол: Какие такие обновления? Ничего не знаю, моя хата с краю! Хочешь обновить — собирай всю цепочку промисов с нуля. Тот же Rx эту проблему пытался решить, но не осилил — любой залётный эксепшен закрывает стрим и оживить его уже нельзя — только пересозданием всего дерева заново.
mayorovp
14.01.2019 10:34Выглядит так, как будто Future, точно так же как и Promise, порождает ровно одно значение, в то время как стрим может породить произвольное количество значений.
В отличии от Promise, Future может быть запущена несколько раз — но на каждый запуск будет генерироваться ровно 1 результат.
sshikov
12.01.2019 19:50>.map(Function) — нет, это не Array.map, это функция трансформации, аналогичная Promise.then
Хм. А какая, простите, разница? Array.map это и есть трансформация массива при помощи функции, разве нет?ArturAralin
12.01.2019 19:58В обоих случаях .map трансформирует данные, но в случае Array.map обрабатывает функция-трансформатор будет применена к каждому элементу массива, а Future.map ко всему объекту (будь это массив, объект или примитив) значения целиком
sshikov
12.01.2019 20:06По-прежнему не вижу разницы. Если вы вспоминаете монады, то приходите к выводу, что map применяется к объекту, который содержит монада (как контейнер для типа). Future содержит один объект, map применяет функцию к нему, Array применяет функцию к массиву, потому что содержит массив. По сути это одно и тоже, и в этом в значительной степени прелесть монад.
Просто Future это такая монада, которая содержит объект, значение которого когда-нибудь будет нам доступно (еще не получили), соответственно транформация применяется по факту получения.
То есть, если подойти к этому как к монаде — то разницы никакой и нет.ArturAralin
12.01.2019 20:14Если подойти как к монаде — то разницы нет.
Ремарка была сделана для того, чтобы не было путаницы с Array.map, т.к. в данном случае Future рассматривалась как альтернатива Promise
angly
12.01.2019 20:30+3Это будет краткий обзор на то, как создавать функции, как обрабатывать ошибки и чуть-чуть про параллелизм.
Стоило начать с того, зачем ленивые функции вообще нужны, какие задачи решают и в чем их преимущества перед обычными промисами.
Future — альтернатива Promise, имеющая куда более мощный API, позволяющий реализовать отмену выполнения (cancellation), безопасную «рекурсию», «безошибочное» выполнение (используя Either) и ещё маленькую тележку крутых возможностей.
И ожидал увидеть хоть что-нибудь из этого списка крутых возможностей, а не просто реимплементации простейших примеров с промисов на ленивые функции.ArturAralin
12.01.2019 20:34-5Статья не про то, что такое ленивые функции и как их применять. На мой взгляд, это отдельная тема тянущая на отдельную
книгустатьюangly
12.01.2019 21:02+2Статья не про то, что такое ленивые функции и как их применять.
Однако, она называется «Ленивые функции в JavaScript».
Вопрос в том, для какой аудитории эта статья.
Возможно, для тех, кто использует ленивые функции на других языках, но хочет узнать, как это делается в JavaScript.
Или для людей, уже использующих функциональный подход в JavaScript (хотя, думаю, такие люди уже нашли для себя оптимальные библиотеки/способы создания ленивых функций).
А вот у людей, программирующих на JavaScript, но очень смутно знакомых с функциональным программированием в целом и с ленивыми функциями в частности, но которые не прочь об этом узнать (к числу которых я отношусь), статья вызывает больше вопросов, чем ответов. В чем преимущества ленивости, в каких задачах они себя проявляют, синхронно ли это работает или только асинхронно, что такое «безопасная рекурсия» или «безошибочное выполнение» и т.д.
impwx
12.01.2019 20:44+6Схема именования — как будто автора за каждый лишний символ кто-то бьет по рукам. Всякие
tryP
,encaseN
иchainRej
еще ладно, но сократитьapply
доap
— такого я еще не видел.
TheWinterMan
Я бы еще обратил внимание, что библиотека написана на mJS, а это разновидность Javascript заточенная под микроконтроллеры из нее вырезаны некоторые функции стандартной библиотеки и, по идеи, она может работать быстрее стандартных промисов, но это не точно.
morsic
Это не тот mjs
nodejs.org/api/esm.html
TheWinterMan
Ошибся, извинияюсь