Привет!


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


  1. TheWinterMan
    12.01.2019 18:40
    -1

    Я бы еще обратил внимание, что библиотека написана на mJS, а это разновидность Javascript заточенная под микроконтроллеры из нее вырезаны некоторые функции стандартной библиотеки и, по идеи, она может работать быстрее стандартных промисов, но это не точно.


    1. morsic
      12.01.2019 19:30
      +1

      Это не тот mjs
      nodejs.org/api/esm.html


      1. TheWinterMan
        14.01.2019 20:10

        Ошибся, извинияюсь


  1. vintage
    12.01.2019 19:45

    Это же стримы типа RxJS.


    1. ArturAralin
      12.01.2019 19:59

      Stream из RxJS не то, чем является Future. Просто они похожи способом трансформации данных


      1. vintage
        12.01.2019 21:22

        Ну а в чём отличие?


        1. ArturAralin
          12.01.2019 21:40

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


          1. napa3um
            12.01.2019 23:12

            Не очень понятно, как «оправдывает» вас бэкенд-разработка, rxJS прекрасно работает и на бэкенде. И да, Future вполне себе реализовывает подмножество возможностей rxJS, и если в проекте уже внедрён rxJS, то нет никаких причин для внедрения ещё и Fluture. Всё, что можно описать на Fluture, опишется на rxJS примерно таким же количеством кода.


            1. ArturAralin
              13.01.2019 00:24

              Не очень понятно, как «оправдывает» вас бэкенд-разработка, rxJS прекрасно работает и на бэкенде

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


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

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


              1. napa3um
                13.01.2019 00:39
                -2

                1) Вам явно не стоит хвастаться своей практикой.
                2) Неповоротливым монолитным ломом является как раз Fluture, и если в проекте не нужно бОльших возможностей, бандл с rxJS получится меньше, при этом если нужно — то всё уже готово. А код семантически получится эквивалентный Флутеру, никакой дополнительной когнитивной нагрузки rxJS от вас не потребует (если вам от неё нужно только то, что нужно от Флутера).
                3) Не воспринимайте вопросы или замечания собеседников о библиотеке как претензии к вам лично или вашему незнанию альтернатив. Или вы как-то относитесь к разработке этой библиотеки? (Тогда тем более стОит быть чуть потерпимее.)


                1. ArturAralin
                  13.01.2019 00:51
                  -1

                  1) Я, пожалуй, сам решу делать мне это или нет
                  2) Кажется, вы судите только из специфики своей работы и инструментов, которые вы используете. Думаю вам стоит пойти в репозиторий обоих инструментов (RxJS и Fluture) и почитать об их предназначение и особенностях
                  3) <не совсем понял, что вы тут имели в виду>


                  1. napa3um
                    13.01.2019 00:58

                    1) Я лишь даю вам дополнительную информацию для принятия вами самостоятельного решения.
                    2) rxJS использую регулярно, ещё с версии 3, Флатер пощупал тоже, и для этого мне таки пришлось сходить в его репозиторий и почитать документацию. И размер бандла тоже не телепатически выяснял, конечно.
                    3) Ничего страшного.


                1. nsinreal
                  13.01.2019 01:06

                  Вам явно не стоит хвастаться своей практикой.
                  Вы так говорите, как будто у FRP область применения шире, чем использование в академических статьях, pet-projectах и едких комментариях на хабре.

                  Если без шуток, то FRP преследует злой рок: даже там где он применяется, от него начинают потихоньку избавляться.


                  1. napa3um
                    13.01.2019 01:11

                    Если без шуток, то Observables из Rx проталкиваются в стандарт ES (Микрософт, автор Rx, вхож в комитет по стандартизации). Если вам не нужен FRP — ничего страшного, не нужно пытаться убеждать себя, что и другим оно не нужно.


                    1. nsinreal
                      13.01.2019 01:58

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

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

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


                1. nsinreal
                  13.01.2019 02:07

                  А код семантически получится эквивалентный Флутеру, никакой дополнительной когнитивной нагрузки rxJS от вас не потребует (если вам от неё нужно только то, что нужно от Флутера).

                  Это не так. Код, который продьюсит данные — он пожалуй, действительно, получится тем же самым. Но код, который использует данные — он получится совершенно другим.
                  В отличии от Promise/Future, Stream может содержать множество значений, даже бесконечность. И либо вы пишите код, который делает бесполезную работу (чтобы соблюсти семантику); либо вы нарушаете семантику и рискуете в будущем получить неработоспособный код.


                  1. napa3um
                    13.01.2019 15:45
                    -1

                    Используемый код получит Observable, который в случае возврата лишь одного значения эквивалентен Promise/Future. Не пишите чепухи, Observable — это обобщение любых асинхронных источников данных. Если хотите защититься от утечек и гарантировать запуск обработчика лишь один раз — примените оператор first (хоть прям по месту использования обсервабла).


                    1. 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?


                      1. napa3um
                        13.01.2019 19:43
                        -1

                        Да что за бред-то? В вашем примере сломается и Фьюча, а вот Обсервабл как раз легко расширить на такой в ариант. Если изначально источник данных был промисовый, то Обсервабл не родит ничего больше одного сигнала, а в случае «настоящего» Обсервабла если вам стало интересно, то, значит, уже и задача поменялась и перестала укладываться в семантику Промиса/Фьючи. Для использования Обсервабла в качестве Промиса/Фьючи никаких особых знаний о Скедулере иметь не нужно.

                        Из диалога с вами ретируюсь, дальше без меня.


                        1. nsinreal
                          13.01.2019 19:52

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

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

                          Для использования Обсервабла в качестве Промиса/Фьючи никаких особых знаний о Скедулере иметь не нужно
                          А я ведь именно про скедулер вообще ничего не говорил. Я упомянул про hot/cold observables в контексте того, что Angular2+ явно проиллюстрировал проблему того, что никто не ожидал hot observables от HttpClient и не умел с ними работать.


                      1. vintage
                        13.01.2019 19:48

                        Можно подумать для Fluture не нужны все эти debounce, throttle, 19 filtering, 23 transformation, 12 combination операторов.


                        1. nsinreal
                          13.01.2019 19:54

                          Не нужны большинство из них.
                          Ну или вам придется просветить меня: зачем debounce/throttle в семантике одного отложенного значения? Они же заточены под «множество» значений


                          1. vintage
                            13.01.2019 22:46

                            Затем чтобы не приходилось все места очередного запуска этого «одного отложенного вычисления» заворачивать вручную в debounce/throttle.


                            1. nsinreal
                              13.01.2019 22:55

                              Омг, вам говорят про только одно значение, а вы говорите — а вот там извне будет много значений. Не, не будет. Потому что инструмент выбран соответственно задаче и нет соблаза изменить задачу под инструмент.


                              1. vintage
                                13.01.2019 23:50

                                Что за мифическая задача такая, где никогда не надо перезагружать данные?


                                1. nsinreal
                                  14.01.2019 00:52

                                  • Обновлений практически нет — пример: всякие редакторы (редактор слайдов, графиков и т.д.) — необходимые ресурсы загружаются при инициализации; Перезагрузка не нужна.
                                  • Обновления происходят редко — пример: многие новостные порталы. Обновление делегируется браузеру (F5)
                                  • Предполагается короткое время жизни UI — пример: данные подгружаются в попапе, который будет закрыт через 10-30 секунд (попап сохранения в Google Drive). Перезагрузка не нужна.
                                  • Загрузка происходит слишком долго — пример: генерация контента на лету. Нужно не debounce делать, а показывать, что что-то грузится очень долго и блочить возможность повторных попыток. Натягивать это на стрим — можно, но не нужно
                                  • Общая несовместимость концепта с перезагрузкой данных — опять же, какой угодно редактор, который не предполагает shared editing.


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


                                  1. vintage
                                    14.01.2019 09:43

                                    всякие редакторы (редактор слайдов, графиков и т.д.)

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


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

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


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

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


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

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


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

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


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

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


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

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


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

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


                        1. ArturAralin
                          13.01.2019 20:13
                          +1

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


                          1. vintage
                            13.01.2019 22:50

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


          1. vintage
            13.01.2019 01:04

            В стримах так-то они тоже отложенные. Я так понимаю это просто одноразовые стримы?


            1. napa3um
              13.01.2019 01:13

              Да всё верно вы поняли, Флутер — это просто подмножество Rx.


        1. mayorovp
          14.01.2019 10:34

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

          В отличии от Promise, Future может быть запущена несколько раз — но на каждый запуск будет генерироваться ровно 1 результат.


  1. sshikov
    12.01.2019 19:50

    >.map(Function) — нет, это не Array.map, это функция трансформации, аналогичная Promise.then
    Хм. А какая, простите, разница? Array.map это и есть трансформация массива при помощи функции, разве нет?


    1. ArturAralin
      12.01.2019 19:58

      В обоих случаях .map трансформирует данные, но в случае Array.map обрабатывает функция-трансформатор будет применена к каждому элементу массива, а Future.map ко всему объекту (будь это массив, объект или примитив) значения целиком


      1. sshikov
        12.01.2019 20:06

        По-прежнему не вижу разницы. Если вы вспоминаете монады, то приходите к выводу, что map применяется к объекту, который содержит монада (как контейнер для типа). Future содержит один объект, map применяет функцию к нему, Array применяет функцию к массиву, потому что содержит массив. По сути это одно и тоже, и в этом в значительной степени прелесть монад.

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

        То есть, если подойти к этому как к монаде — то разницы никакой и нет.


        1. ArturAralin
          12.01.2019 20:14

          Если подойти как к монаде — то разницы нет.
          Ремарка была сделана для того, чтобы не было путаницы с Array.map, т.к. в данном случае Future рассматривалась как альтернатива Promise


  1. angly
    12.01.2019 20:30
    +3

    Это будет краткий обзор на то, как создавать функции, как обрабатывать ошибки и чуть-чуть про параллелизм.

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

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


    1. ArturAralin
      12.01.2019 20:34
      -5

      Статья не про то, что такое ленивые функции и как их применять. На мой взгляд, это отдельная тема тянущая на отдельную книгу статью


      1. angly
        12.01.2019 21:02
        +2

        Статья не про то, что такое ленивые функции и как их применять.

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

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

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

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

        А вот у людей, программирующих на JavaScript, но очень смутно знакомых с функциональным программированием в целом и с ленивыми функциями в частности, но которые не прочь об этом узнать (к числу которых я отношусь), статья вызывает больше вопросов, чем ответов. В чем преимущества ленивости, в каких задачах они себя проявляют, синхронно ли это работает или только асинхронно, что такое «безопасная рекурсия» или «безошибочное выполнение» и т.д.


  1. impwx
    12.01.2019 20:44
    +6

    Схема именования — как будто автора за каждый лишний символ кто-то бьет по рукам. Всякие tryP, encaseN и chainRej еще ладно, но сократить apply до ap — такого я еще не видел.