Ленивые функции в JavaScript

Привет!


Подумал я тут рассказать вам о том, как в 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}

Ссылки



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

Поделиться публикацией

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

    –3
    Я бы еще обратил внимание, что библиотека написана на mJS, а это разновидность Javascript заточенная под микроконтроллеры из нее вырезаны некоторые функции стандартной библиотеки и, по идеи, она может работать быстрее стандартных промисов, но это не точно.
    0
    Это же стримы типа RxJS.
      0
      Stream из RxJS не то, чем является Future. Просто они похожи способом трансформации данных
        0
        Ну а в чём отличие?
          0
          Я не большой специалист в RxJS, т.к. в основном занимаюсь back-end разработкой, но грубо говоря RxJS это про управление событиями и потоками данных, а Future про отложенные вычисления
            –2
            Не очень понятно, как «оправдывает» вас бэкенд-разработка, rxJS прекрасно работает и на бэкенде. И да, Future вполне себе реализовывает подмножество возможностей rxJS, и если в проекте уже внедрён rxJS, то нет никаких причин для внедрения ещё и Fluture. Всё, что можно описать на Fluture, опишется на rxJS примерно таким же количеством кода.
              0
              Не очень понятно, как «оправдывает» вас бэкенд-разработка, rxJS прекрасно работает и на бэкенде

              Я нигде и не говорил, что RxJS не работает на back-end.
              Что касается "оправдания", то на моей практике задачи требующие FRP это редкость, а там, где потенциально этот подход можно использовать, было не целесообразно подключать библиотеку для реализации FRP


              Future вполне себе реализовывает подмножество возможностей rxJS

              Каждый инструмент имеет свое предназначение. Так-то и ломом можно консервные банки открывать

                –7
                1) Вам явно не стоит хвастаться своей практикой.
                2) Неповоротливым монолитным ломом является как раз Fluture, и если в проекте не нужно бОльших возможностей, бандл с rxJS получится меньше, при этом если нужно — то всё уже готово. А код семантически получится эквивалентный Флутеру, никакой дополнительной когнитивной нагрузки rxJS от вас не потребует (если вам от неё нужно только то, что нужно от Флутера).
                3) Не воспринимайте вопросы или замечания собеседников о библиотеке как претензии к вам лично или вашему незнанию альтернатив. Или вы как-то относитесь к разработке этой библиотеки? (Тогда тем более стОит быть чуть потерпимее.)
                  +3
                  1) Я, пожалуй, сам решу делать мне это или нет
                  2) Кажется, вы судите только из специфики своей работы и инструментов, которые вы используете. Думаю вам стоит пойти в репозиторий обоих инструментов (RxJS и Fluture) и почитать об их предназначение и особенностях
                  3) <не совсем понял, что вы тут имели в виду>
                    –4
                    1) Я лишь даю вам дополнительную информацию для принятия вами самостоятельного решения.
                    2) rxJS использую регулярно, ещё с версии 3, Флатер пощупал тоже, и для этого мне таки пришлось сходить в его репозиторий и почитать документацию. И размер бандла тоже не телепатически выяснял, конечно.
                    3) Ничего страшного.
                    +4
                    Вам явно не стоит хвастаться своей практикой.
                    Вы так говорите, как будто у FRP область применения шире, чем использование в академических статьях, pet-projectах и едких комментариях на хабре.

                    Если без шуток, то FRP преследует злой рок: даже там где он применяется, от него начинают потихоньку избавляться.
                      –2
                      Если без шуток, то Observables из Rx проталкиваются в стандарт ES (Микрософт, автор Rx, вхож в комитет по стандартизации). Если вам не нужен FRP — ничего страшного, не нужно пытаться убеждать себя, что и другим оно не нужно.
                        +2
                        Если вам не нужен FRP
                        Не то, чтобы мне не нужно было FRP. Мне не нужны текущие реализации FRP, потому что я не видел реализаций, совместимых с концепцией «понятно где и почему упало». К примеру, у промисов такая же проблема была долгое время, но в меньших масштабах. (Fluture тоже будет иметь такие же проблемы)

                        Помимо всего прочего, FRP выглядит просто на словах, но имеет дохрена тонкостей, которые раскрываются с каждым днем использования (сужу по rxjs). Мне кажется, что порог входа слишком высок, а область применения слишком мала.

                        не нужно пытаться убеждать себя, что и другим оно не нужно.
                        О, почти классика. Сначала вы обесценили опыт человека. Потом на едкое возражение «узкая область применения, чтобы это обесценивало чей-то опыт» вы выставляете виноватым опоннента в том, что он обесценивает FRP. Т.е. «все кто не используют FRP — дураки, а если кто не согласен, то он дурак». Прекрасная петля, почти как жопа Хэнка.
                      0
                      А код семантически получится эквивалентный Флутеру, никакой дополнительной когнитивной нагрузки rxJS от вас не потребует (если вам от неё нужно только то, что нужно от Флутера).

                      Это не так. Код, который продьюсит данные — он пожалуй, действительно, получится тем же самым. Но код, который использует данные — он получится совершенно другим.
                      В отличии от Promise/Future, Stream может содержать множество значений, даже бесконечность. И либо вы пишите код, который делает бесполезную работу (чтобы соблюсти семантику); либо вы нарушаете семантику и рискуете в будущем получить неработоспособный код.
                        –1
                        Используемый код получит Observable, который в случае возврата лишь одного значения эквивалентен Promise/Future. Не пишите чепухи, Observable — это обобщение любых асинхронных источников данных. Если хотите защититься от утечек и гарантировать запуск обработчика лишь один раз — примените оператор first (хоть прям по месту использования обсервабла).
                          +2
                          примените оператор 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?
                            –2
                            Да что за бред-то? В вашем примере сломается и Фьюча, а вот Обсервабл как раз легко расширить на такой в ариант. Если изначально источник данных был промисовый, то Обсервабл не родит ничего больше одного сигнала, а в случае «настоящего» Обсервабла если вам стало интересно, то, значит, уже и задача поменялась и перестала укладываться в семантику Промиса/Фьючи. Для использования Обсервабла в качестве Промиса/Фьючи никаких особых знаний о Скедулере иметь не нужно.

                            Из диалога с вами ретируюсь, дальше без меня.
                              0
                              Из диалога с вами ретируюсь, дальше без меня.
                              Вы слились, но я все же напишу, почему натягивать на Observable семантику Promise/Future — плохая идея. Остальным может быть полезно.

                              В вашем примере сломается и Фьюча, а вот Обсервабл как раз легко расширить на такой в ариант
                              Есть разница между «код, который не компилируется» и «код, который притворяется, что он работает».

                              Для использования Обсервабла в качестве Промиса/Фьючи никаких особых знаний о Скедулере иметь не нужно
                              А я ведь именно про скедулер вообще ничего не говорил. Я упомянул про hot/cold observables в контексте того, что Angular2+ явно проиллюстрировал проблему того, что никто не ожидал hot observables от HttpClient и не умел с ними работать.
                              0
                              Можно подумать для Fluture не нужны все эти debounce, throttle, 19 filtering, 23 transformation, 12 combination операторов.
                                0
                                Не нужны большинство из них.
                                Ну или вам придется просветить меня: зачем debounce/throttle в семантике одного отложенного значения? Они же заточены под «множество» значений
                                  0
                                  Затем чтобы не приходилось все места очередного запуска этого «одного отложенного вычисления» заворачивать вручную в debounce/throttle.
                                    0
                                    Омг, вам говорят про только одно значение, а вы говорите — а вот там извне будет много значений. Не, не будет. Потому что инструмент выбран соответственно задаче и нет соблаза изменить задачу под инструмент.
                                      0
                                      Что за мифическая задача такая, где никогда не надо перезагружать данные?
                                        0
                                        • Обновлений практически нет — пример: всякие редакторы (редактор слайдов, графиков и т.д.) — необходимые ресурсы загружаются при инициализации; Перезагрузка не нужна.
                                        • Обновления происходят редко — пример: многие новостные порталы. Обновление делегируется браузеру (F5)
                                        • Предполагается короткое время жизни UI — пример: данные подгружаются в попапе, который будет закрыт через 10-30 секунд (попап сохранения в Google Drive). Перезагрузка не нужна.
                                        • Загрузка происходит слишком долго — пример: генерация контента на лету. Нужно не debounce делать, а показывать, что что-то грузится очень долго и блочить возможность повторных попыток. Натягивать это на стрим — можно, но не нужно
                                        • Общая несовместимость концепта с перезагрузкой данных — опять же, какой угодно редактор, который не предполагает shared editing.


                                        Вы знаете, существует потрясно много задач, в которых не нужен debounce.
                                        И блин, если вам нужны обновления, то вместо pull семантики многие используют push-семантику, где не нужен debounce
                                          0
                                          всякие редакторы (редактор слайдов, графиков и т.д.)

                                          Если не троттлить ввод пользователя редактор будет тупить на чуть более сложном графике.


                                          Обновления происходят редко

                                          Не важно редко или часто. Важно, что происходят.


                                          многие новостные порталы.

                                          Блок "последние новости", "активные обсуждения" и тп.


                                          попап сохранения в Google Drive

                                          Это часть приложения, где очень много рилтайм данных. Один попап со статическим текстом погоды не делает.


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

                                          Мы так делали — было норм. Быстро отдаём фейковые заглушки и спокойно ждём загрузки реальных данных.


                                          какой угодно редактор, который не предполагает shared editing

                                          Таких почти не осталось.


                                          существует потрясно много задач, в которых не нужен debounce.

                                          И нет ни одной, где его поддержка была бы лишней. Вы что доказать-то хотите? Что использовать для разных кейсов 2 разных не совместимых апи лучше, чем один обобщённый?


                                          вместо pull семантики многие используют push-семантику

                                          1. Ну и дураки.
                                          2. Rx, Promise, Fluture — это как раз push семантика.
                                  +1

                                  vintage, не нужны, т.к. все эти действия выходят за пределы ответственности Future. Для фильтраций, комбинаций и трансформаций используете .map или .chain и используете те реализации функций, которые вам надо (ну или используйте что-то вроде Ramda или Sanctuary)

                                    0
                                    Так и в стримах в качестве пайпов можно использовать любые реализации. Но они хотя бы не засовывают голову в песок, мол: Какие такие обновления? Ничего не знаю, моя хата с краю! Хочешь обновить — собирай всю цепочку промисов с нуля. Тот же Rx эту проблему пытался решить, но не осилил — любой залётный эксепшен закрывает стрим и оживить его уже нельзя — только пересозданием всего дерева заново.
                      0
                      В стримах так-то они тоже отложенные. Я так понимаю это просто одноразовые стримы?
                        –1
                        Да всё верно вы поняли, Флутер — это просто подмножество Rx.
                      0
                      Выглядит так, как будто Future, точно так же как и Promise, порождает ровно одно значение, в то время как стрим может породить произвольное количество значений.

                      В отличии от Promise, Future может быть запущена несколько раз — но на каждый запуск будет генерироваться ровно 1 результат.
                  0
                  >.map(Function) — нет, это не Array.map, это функция трансформации, аналогичная Promise.then
                  Хм. А какая, простите, разница? Array.map это и есть трансформация массива при помощи функции, разве нет?
                    0
                    В обоих случаях .map трансформирует данные, но в случае Array.map обрабатывает функция-трансформатор будет применена к каждому элементу массива, а Future.map ко всему объекту (будь это массив, объект или примитив) значения целиком
                      +1
                      По-прежнему не вижу разницы. Если вы вспоминаете монады, то приходите к выводу, что map применяется к объекту, который содержит монада (как контейнер для типа). Future содержит один объект, map применяет функцию к нему, Array применяет функцию к массиву, потому что содержит массив. По сути это одно и тоже, и в этом в значительной степени прелесть монад.

                      Просто Future это такая монада, которая содержит объект, значение которого когда-нибудь будет нам доступно (еще не получили), соответственно транформация применяется по факту получения.

                      То есть, если подойти к этому как к монаде — то разницы никакой и нет.
                        0
                        Если подойти как к монаде — то разницы нет.
                        Ремарка была сделана для того, чтобы не было путаницы с Array.map, т.к. в данном случае Future рассматривалась как альтернатива Promise
                    +5
                    Это будет краткий обзор на то, как создавать функции, как обрабатывать ошибки и чуть-чуть про параллелизм.

                    Стоило начать с того, зачем ленивые функции вообще нужны, какие задачи решают и в чем их преимущества перед обычными промисами.
                    Future — альтернатива Promise, имеющая куда более мощный API, позволяющий реализовать отмену выполнения (cancellation), безопасную «рекурсию», «безошибочное» выполнение (используя Either) и ещё маленькую тележку крутых возможностей.

                    И ожидал увидеть хоть что-нибудь из этого списка крутых возможностей, а не просто реимплементации простейших примеров с промисов на ленивые функции.
                      –7
                      Статья не про то, что такое ленивые функции и как их применять. На мой взгляд, это отдельная тема тянущая на отдельную книгу статью
                        +6
                        Статья не про то, что такое ленивые функции и как их применять.

                        Однако, она называется «Ленивые функции в JavaScript».

                        Вопрос в том, для какой аудитории эта статья.

                        Возможно, для тех, кто использует ленивые функции на других языках, но хочет узнать, как это делается в JavaScript.

                        Или для людей, уже использующих функциональный подход в JavaScript (хотя, думаю, такие люди уже нашли для себя оптимальные библиотеки/способы создания ленивых функций).

                        А вот у людей, программирующих на JavaScript, но очень смутно знакомых с функциональным программированием в целом и с ленивыми функциями в частности, но которые не прочь об этом узнать (к числу которых я отношусь), статья вызывает больше вопросов, чем ответов. В чем преимущества ленивости, в каких задачах они себя проявляют, синхронно ли это работает или только асинхронно, что такое «безопасная рекурсия» или «безошибочное выполнение» и т.д.
                      +8
                      Схема именования — как будто автора за каждый лишний символ кто-то бьет по рукам. Всякие tryP, encaseN и chainRej еще ладно, но сократить apply до ap — такого я еще не видел.

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое