Трансдьюсеры в JavaScript. Часть вторая

    В первой части мы остановились на следующей спецификации: Трансдьюсер — это функция принимающая функцию step, и возвращающая новую функцию step.

    step⁰ → step¹
    

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

    result⁰, item → result¹
    

    Чтобы получить новый текущий результат в функции step¹, нужно вызвать функцию step⁰, передав в нее старый текущий результат и новое значение, которое мы хотим добавить. Если мы не хотим добавлять значение, то просто возвращем старый результат. Если хотим добавить одно значение, то вызываем step⁰, и то что он вернет возвращаем как новый результат. Если хотим добавить несколько значений, то вызываем step⁰ несколько раз по цепочке, это проще показать на примере реализации трансдьюсера flatten:

    function flatten() {
      return function(step) {
        return function(result, item) {
          for (var i = 0; i < item.length; i++) {
            result = step(result, item[i]);
          }
          return result;
        }
      }
    }
    
    var flattenT = flatten();
    
    _.reduce([[1, 2], [], [3]], flattenT(append), []); // => [1, 2, 3]
    

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

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

    Итак, сейчас мы можем:
    1. Изменять элементы (прим. map)
    2. Пропускать элементы (прим. filter)
    3. Выдавать для одного элемента несколько новых (прим. flatten)


    Преждевременное завершение


    Но что, если мы хотим прервать весь процесс посередине? Т.е. реализовать take, например. Для этого Рич предлагает заворачивать возвращаемое значение в специальную обертку «reduced».

    function Reduced(wrapped) {
      this._wrapped = wrapped;
    }
    Reduced.prototype.unwrap = function() {
      return this._wrapped;
    }
    Reduced.isReduced = function(obj) {
      return (obj instanceof Reduced);
    }
    
    function take(n) {
      return function(step) {
        var count = 0;
        return function(result, item) {
          if (count++ < n) {
            return step(result, item);
          } else {
            return new Reduced(result);
          }
        }
      }
    }
    
    var first5T = take(5);
    

    Если мы хотим завершить процесс, то, вместо того чтобы вернуть очередной result как обычно, возвращаем result, завернутый в Reduced. Сразу обновим сигнатуру функции step:

    result⁰, item → result¹ | reduced(result¹)
    

    Но функция _.reduce уже не сможет обрабатывать такую версию трансдьюсеров. Придется написать новую.

    function reduce(coll, fn, seed) {
      var result = seed;
      for (var i = 0; i < coll.length; i++) {
        result = fn(result, coll[i]);
        if (Reduced.isReduced(result)) {
          return result.unwrap();
        }
      }
      return result;
    }
    

    Теперь можно применить трансдьюсер first5T.

    reduce([1, 2, 3, 4, 5, 6, 7], first5T(append), []);    // => [1, 2, 3, 4, 5]
    


    Еще придется добавлять проверку Reduced.isReduced(result) в трансдьюсеры, которые несколько раз вызывают step (прим. flatten). Т.е. если во flatten при очердном вызове step нам вернут результат завернутый в Reduced, мы обязаны завершить свой цикл, и вернуть этот завернутый результат.

    Состояние


    Еще одна важная деталь, трансдьюсер take имеет состояние. Он запоминает сколько элементов уже через него прошло. Чтобы всё работало правильно, этот счетчик нужно создавать именно в том месте, где он создан в примере (см. var count), т.е. внутри функции, которая возвращает step. Если бы это была, например, глобальная переменная, то мы бы считали элементы для всех трансдьюсеров типа take в одном счетчике, и получали бы неправильный результат.

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

    function transduce(transducer, append, seed, coll) {
      var step = transducer(append);  // В момент вызова этой функции создаются состояния.
                                      // step содержит в себе счетчик,
                                      // и его (step) следует использовать только в рамках
                                      // этого цикла обработки коллекции, после чего уничтожить.
      return reduce(coll, step, seed);
    }
    
    transduce(first5T, append, [], [1, 2, 3, 4, 5, 6, 7]);    // => [1, 2, 3, 4, 5]
    


    Завершение


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

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

    Чтобы можно было такое сделать Рич, предлагает добавить еще один вариант функции step, в который не передается следующее значение, а передается только текущий результат. Этот вариант будет вызываться в конце обработки коллекции, если не было преждевременного завершения.

    В clojure эти две функции объединяются в одну, мы в JavaScript тоже можем так сделать.

    function step(result, item) {
      if (arguments.length === 2) { // обычный вызов
        // возвращаем step(result, item) или что вам нужно
      }
      if (arguments.length === 1) { // завершительный вызов
        // Здесь необходимо вызвать step c одним аргументом, чтобы передать завершающий сигнал дальше.
        // Но если мы хотим что-то добавить в коллекцию в конце,
        // то мы должны сначала вызвать step с двумя аргументами, а потом с одним.
    
        // ничего не добавляем
        return step(result);
    
        // что-то добавляем
        result = step(result, что-то);
        return step(result);
      }
    }
    

    Обновим сигнатуру функции step, теперь у нее два варианта в зависимости от числа аргументов:

    result⁰ → result¹ *
    result⁰, item → result¹ | reduced(result¹)
    
    * я не уверен может ли здесь возвращаться reduced(result¹), из выступления Рича это не ясно. Будем пока считать что не может.
    


    Все трансдьюсеры должны поддерживать обе операции — обычный шаг и завершительный вызов. Также функции transduce() и append() придется обновить, добавив поддержку завершительного вызова.

    function transduce(transducer, append, seed, coll) {
      var step = transducer(append);
      var result = reduce(coll, step, seed);
      return step(result);
    }
    
    function append(result, item) {
      if (arguments.length === 2) {
        return result.concat([item]);
      }
      if (arguments.length === 1) {
        return result;
      }
    }
    


    Итак, вот реализация partition (разбивает коллекцию на маленькие коллекции):

    function partition(n) {
      if (n < 1) {
        throw new Error('n должен быть не меньше 1');
      }
      return function(step) {
        var cur = [];
        return function(result, item) {
          if (arguments.length === 2) {
            cur.push(item);
            if (cur.length === n) {
              result = step(result, cur);
              cur = [];
              return result;
            } else {
              return result;
            }
          }
          if (arguments.length === 1) {
            if (cur.length > 0) {
              result = step(result, cur);
            }
            return step(result);
          }
        }
      }
    }
    
    var by3ItemsT = partition(3);
    
    transduce(by3ItemsT, append, [], [1,2,3,4,5,6,7,8]);   // => [[1,2,3], [4,5,6], [7,8]]
    


    Инициализация


    Рич еще предлагает добавить возможность для трансдьюсеров создавать начальное пустое значение результата. (Мы везде для этих целей использовали пустой массив, который явно передавали сначала в reduce, а потом в transduce.)

    Для этого нужно добавить еще один вариант функции step — вообще без параметров. Если step вызывается без параметров, она должна вернуть начальное значение, например пустой массив.

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

    Обновим сигнатуру функции step.

    → result
    result⁰ → result¹
    result⁰, item → result¹ | reduced(result¹)
    

    Обновим функции transduce() и append()

    function transduce(transducer, append, coll) {
      var step = transducer(append);
      var seed = step();
      var result = reduce(coll, step, seed);
      return step(result);
    }
    
    function append(result, item) {
      if (arguments.length === 2) {
        return result.concat([item]);
      }
      if (arguments.length === 1) {
        return result;
      }
      if (arguments.length === 0) {
        return [];
      }
    }
    

    И перепишем для примера генератор трансдьюсеров map.

    function map(fn) {
      return function(step) {
        return function(result, item) {
          if (arguments.length === 2) {
            return step(result, fn(item));
          }
          if (arguments.length === 1) {
            return step(result);
          }
          if (arguments.length === 0) {
            return step();
          }
        }
      }
    }
    

    Получается мы просто перенесли пустой массив из параметра transduce() внутрь append(), на первый взгляд это ненужное действие, но это дало нам возможность создавать трансдьюсеры, которые добавляют что-то в начало коллекции (как те что добавляют в конец, только наоборот).

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

    Итоги


    На этом всё. Я пересказал весь доклад Рича Хикки. И, как я понимаю, это пока вообще всё что можно рассказать про трансдьюсеры.

    Подытожим еще раз что мы получили. Мы получили универсальный способ создавать операции над коллекциями. Эти операции могут: изменять элементы (map), пропускать элементы (filter), размножать элементы (flatten), иметь стейт (take, partition), преждевременно завершать обработку (take), добавлять что-то в конце (partition) и добавлять что-то вначале. Все эти операции мы можем легко объединять с помощью compose, и использовать как на обычных коллекциях, так, например, и в FRP. Кроме того, это всё будет работать быстро и потреблять мало памяти, т.к. не создается временных коллекций.

    Это всё круто! Но как нам начать их использовать? Проблема в том, что чтобы использовать трансдьюсеры по максимуму, JavaScript сообщество должно договориться о спецификации (а мы это умеем, да? :-). Тогда мог бы реализоваться крутой сценарий, при котором библиотеки для работы с коллекциями (underscore и пр.) будут уметь создавать трансдьюсеры, а другие билиотеки, которые не совсем про коллекции (напр. FRP), будут просто поддерживать трансдьюсеры.

    Спецификация которую предлагает Рич, на первый взгляд, неплохо ложится на JavaScript, за исключением детали про Reduced. Дело в том, что в Clojure уже есть глобальный Reduced (он там уже давно), а в JavaScript нет. Его, конечно, легко создать, но каждая библиотека, будет создавать свой Reduced. В итоге если я, например, захочу добавить поддержку трансдьюсеров в Kefir.js, мне придется добавлять поддержку трансдьюсеров-underscore, трансдьюсеров-LoDash и т.д. Reduced — это слабое место спецификации предлагаемой Ричем.

    Другой сценарий — появление разных библиотек про трансдьюсеры, у каждой из которых будет своя спецификация. Тогда мы сможем получить только часть преимуществ. Уже есть библиотека transducers.js, в ней конечно создан свой Reduced, и пока нет поддержки завершительного и начального вызовов, и неизвестно в каком виде автор их добавит.

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

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

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

      0
      После прочтения Functional Javascript — без underscore\lodash ни одного проекта, а реактивность сейчас тоже куда ни глянь. И если будут удобные мосты между популярными инструментами — это будет победа.
      Kifir для моделек например, React — View, и underscore для декларативного описания логики.
        0
        Странно, вместо конструкции `Reduced` можно же реализовать `take/limit` на подобии `filter`. Или я что-то упустил?
        Ну a интерфейс функций для преобразователей нужно взять аналогичный стандартному `reduce`: `([[CurrentCtx]],[[Item]], [[Accessor]],[[RootCtx]], )`, где `Accessor` это индекс (в случае итерации по массиву).
          +1
          Да, вы упустили время. take(5), который выполняет полный перебор по коллекции из миллиона элементов — это очень грустно.
            0
            Странно, вместо конструкции `Reduced` можно же реализовать `take/limit` на подобии `filter`. Или я что-то упустил?

            Тогда остальная часть коллекции тоже будет обрабатываться, хоть и все элементы выбросит take. А коллекции могут быть и бесконечными.
              0
              Но мы же можем на нужном шаге вернуть `void 0`, тем самым сообщив нашему итератору, что нужно завершить работу и вернуть коллекцию из предыдущего шага.
              /cc mayorovp
                0
                В таком случае ваш `void 0` является замаскированным Reduced.
                  +1
                  Только в Reduced завернуто значение еще.
                    0
                    Это не играет роли, тут главная идея — специальное значение, которое прерывает итерацию.
                      0
                      Как не играет, значение нужно же вернуть. Вы скажите конкретно откуда вы хотите возвращать этот undefined?
                      Кроме того вы резервируете undefined для этой цели, а он вполне может использоваться как обычное значение в библиотеке использующей трансдьюсер.
                        +1
                        Конкретно я не хочу возвращать undefined. И насчет того, что undefined может использоваться как обычное значение — я тоже согласен. Я говорю лишь о том, что undefined ничем не лучше Reduced.

                        Окончательное же значение коллекции tenbits предлагает брать с предыдущего шага. Это будет работать и даже ничуть не усложнит take.

                          0
                          Сорри, перепутал. Там result не обязательно будет тот же самый. Можно вернуть new Reduced(step(result, что-то)).
                            0
                            Да, а можно — вернуть просто step(result, что-то) — а следующий шаг отменить, как написал tenbits. Будет то же самое.
                              0
                              Мы тогда на один элемент больше обработаем, а в FRP, например, это может быть важно (позже по времени завершится поток).

                              И может где-то что-то еще не сойдется, сейчас не приходит в голову.
                                0
                                Позже по времени — не страшно: в конце концов, ждать-то мы его завершения не будем. А вот тот факт, что в таком случае возможен, к примеру, лишний запрос к серверу, действительно печален.
                                  0
                                  Можем и ждать, завершение потока может что-то значить в FRP иногда.
                                  –2
                                  Больше чем с `Reduced` мы не обработаем, в случае с прерыванием `Reduced` и `void 0` — эквивалентны. Там где мы возвращаем `new Reduced` мы можем вообще ничего не возвращать, поэтому там тот `else` просто удаляется, а там где у нас `isReduced` появится `=== void 0`. как то так
                                    0
                                    Постойте, сам хоть кто-то попробовал подставить `void 0`? Дружно открываем devtools и копируем:

                                    Код
                                    var array = [1, 2, 3, 4, 5, 6, 7];
                                    (function() {
                                        var i = 0;
                                        function reduce(coll, fn, seed) {
                                            var result = seed;
                                            for (; i < coll.length; i++) {
                                                result = fn(result, coll[i]);
                                                if (Reduced.isReduced(result)) {
                                                    return result.unwrap();
                                                }
                                            }
                                            return result;
                                        }
                                        function Reduced(wrapped) {
                                            this._wrapped = wrapped;
                                        }
                                        Reduced.prototype.unwrap = function() {
                                            return this._wrapped;
                                        }
                                        Reduced.isReduced = function(obj) {
                                            return (obj instanceof Reduced);
                                        }
                                        function take(n) {
                                            return function(step) {
                                                var count = 0;
                                                return function(result, item) {
                                                    if (count++ < n) {
                                                        return step(result, item);
                                                    } else {
                                                        return new Reduced(result);
                                                    }
                                                }
                                            }
                                        }
                                        var first5T = take(5);
                                        var result = reduce(array, first5T(append), []);
                                        console.log('> Reduced. Loops: %d Result: ', ++i, result);
                                    }());
                                    
                                    (function() {
                                        var i = 0;
                                        function reduce(coll, fn, ctx) {
                                            var prev = ctx;
                                            for (; i < coll.length; i++) {
                                                ctx = fn(ctx, coll[i]);
                                                if (ctx === void 0) 
                                                    return prev;
                                                prev = ctx;
                                            }
                                            return ctx;
                                        }
                                        function take(n) {
                                            return function(step) {
                                                var count = 0;
                                                return function(result, item) {
                                                    if (count++ < n) 
                                                        return step(result, item);
                                                }
                                            }
                                        }
                                        var first5T = take(5);
                                        var result = reduce(array, first5T(append), []);
                                        console.log('> Void 0. Loops: %d Result: ', ++i, result);
                                    }());
                                    
                                    function append(ctx, x) {
                                        return ctx.concat([x]);
                                    }
                                    


                                      +1
                                      У вас take неоптимален. Попробуйте вот этот:
                                          function take(n) {
                                              if (n==0) 
                                                  return function(step) { 
                                                      return function(result, item) {
                                                          return new Result(result);
                                                      }
                                                  }
                                              else
                                                  return function(step) {
                                                      var count = 0;
                                                      return function(result, item) {
                                                          if (++count < n)
                                                              return step(result, item);
                                                          else
                                                              return new Reduced(step(result, item));
                                                      }
                                                  }
                                          }
                                      
                                      и добейтесь того же самого поведения с `void 0`

                                      PS
                                      > Reduced. Loops: 5 Result: [1, 2, 3, 4, 5]
                                      > Void 0. Loops: 6 Result: [1, 2, 3, 4, 5]
                                        0
                                        Также можно, правда `append` нужно сделать мутатором, что кстати и используется в «transducers.js», а иначе, если на каждом шагу создавать новый массив через `concat`, то о никакой производительности на коллекциях даже в тысячу элементов говорить никак нельзя.
                                        function take(n) {
                                            return function(step) {
                                                var count = 0;
                                                return function(result, item) {
                                                    if (++count < n)
                                                        return step(result, item);
                                                    else if (count === n)
                                                        step(result, item);
                                                }
                                            }
                                        }
                                        function append(ctx, x) {
                                            ctx.push(x);
                                            return ctx;
                                        }
                                        


                                        Конечно же с `Reduced` у нас больше возможностей. Для подхода с дополнительными классами такое поведения — это самая нижняя планка, а вот для `void 0` — это максимум, что мы можем выжать. Но нужно ли вводить дополнительные классы/обьекты, если вполне пригодно можно обойтись без них? Хотя этот вопрос спекулятивный, так же как и ваше предложение мне изучить, что такое state.
                                          0
                                          В таком случае придется все трансдьюсеры писать в стиле мутаторов, даже те, которые изначально таковыми не являлись (к примеру, count). А в таком случае передавать ему первый элемент бессмысленно — то есть надо упрощать дальше.

                                          А дальше мы получим, между прочим, поток. Внезапно :)
                                        0
                                        Добалю к тому что написал mayorovp.

                                        Резервировать undefined всё таки неправильно, посмотрите мой пример с потоком habrahabr.ru/post/237733/#comment_7998887, я там использую null (потому что результат никуда сохранять не нужно), но кто-то может захотеть использовать undefined, и я считаю он имеет полное право использовать undefined.

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

                                        Думаю идеальным было бы сделать такой редьюсд `{__transducersReduced__: true, wrapped: result}`, тогда мы наложим минимум ограничений, и избавимся от привязки к конкретной библиотеке.
                                          0
                                          Точнее в примере с потоком уже используется `undefined`. Там все step функции всегда возвращают `undefined`. Я даже и не заметил :)
                              0
                              Для порядка всёже спрошу: а что если у меня изначально коллекция из элементов класса Reduced- как мне её трансдьюсить?)
                                0
                                Нельзя такую коллекцию. Reduced мы зарезервировали, в этом нет ничего страшного.
                          0
                          Фишка `void 0` в том что это даже не значение. Функция может абсолютно ничего не возвращать и это уже будет `result === void 0`. Таким образом не нужно вводить дополнительную «обёртку» `Reduce`, и не нужно каждый раз вызывать instanceof. На коллекциях в миллион элементов это будет существенным улучшением.
                          0
                          Кстати, в LoDash для прерывания используется return false. Оба варианта — слаботипизированные breaker/Reduced :)
                          Ещё можно использовать объект, ссылка на него будет уникальной, а в новом стандарте вообще есть тип Symbol.

                          Тут главная разница в том, что Reduced содержит не только метку, но и сам результат, который далее анбоксится. Все остальные варианты перекладывают задачу на управляющую функцию transduce. Вероятно, Ричу более нравится «чистый» подход, с хранением результата, а мы в JS привыкли к флаговым значениям.
                            +1
                            Ещё раз, `void 0 / undefined` — это отсутствие какого либо значение, а `null / false / Symbol / Reduced ` — это какое-то значение. В javascript это существенная разница. И учитывая, что нашa runner-функция ожидает `nextValue`, то вполне логично если она ничего не получила, тут же прервёт итерацию и вернёт `currentValue`.
                            Ещё одним недостатком `Reduced/Symbol` является то, что мы в систему добавляем новый апи, это значит, что наша `take` функция становится зависимой от `Reduced/Symbol`. Сами понимаете чем это чревато, а вот в случае с `void 0` — это лишь вопрос поведения.

                            В дополнение, `Reduced` вносит State в систему, поэтому я бы не назвал это чистым подходом. Чем меньше состояний тем лучше.
                              0
                              Хм, знаете, я не думаю, что передача Reduced через возвращаемое значение это стейт. Это просто бокс для значения. А вот хранение результата где-то в вызывающей функции и его использовании при появлении void 0, вот это уже стейт.

                              Да, хочу сказать, что я лично не выступаю за вариант с обёрткой, я, скорее, придерживаюсь вашей точки зрения, что лучше использовать существующее значение (undefined), тем более оно подходит по семантике. Так что я ни в коем случае не спорю с вами, скорее пытаюсь выжать из идеи всё до конца.
                                0
                                Разумеется), я всегда придерживаюсь позиции, что абсолютной истины нет, поэтому никогда яростно не отстаиваю какую нибудь точку зрения.

                                > А вот хранение результата где-то в вызывающей функции

                                Результат хранится в рамках одной функции, и лишь хранит ссылку на `current`/`next` значения, поэтому это состоянием назвать нельзя, так как обычно подразумевается, что состояние находится в системе (между некоторыми функциями/библиотеками/запросами). И вот когда мы создаем какой-то объект который содержит значение системы (в данном случае `reduced/notReduced`) и передает другим элементам системы — вот это и есть State. А то, что наш `runner` заканчивает работу при `void 0` — так это лишь поведение. `void 0` не содержит в себе никакого значения, поэтому и состоянием быть не может. Хотя тут можно философствовать, что «отсутствие состояния — является состоянием» )
                                  0
                                  И вот когда мы создаем какой-то объект который содержит значение системы (в данном случае `reduced/notReduced`) и передает другим элементам системы — вот это и есть State
                                  Пожалуйста, изучите, что именно называется состоянием.
                          +3
                          В порядке бреда — можно выкинуть значение исключением, тогда дополнительные объекты не нужны :-)
                            +1
                            Можно :) Но тогда придётся создавать и ловить конкретное исключение, чтобы не маскировать другие исключения в случае, например, каких-то ошибок в логике функции.
                        +3
                        Если честно, то не вижу существенных преимуществ перед генераторами. За исключением некоторых деталей, на генераторах можно делать аналогичные вещи:
                        1. Преждевременное завершение обработки коллекции как внутри генератора (по логике или с помощью посылаемого в генератор значения), так и снаружи — для цикла for-of это break, в остальных случаях просто перестаём вызывать у генератора next
                        2. Генераторы и компания, работают с любыми коллекциями, главное, чтобы они реализовывали Iterator Protocol
                        3. Генераторы умеют «выдавать для одного элемента несколько новых (прим. flatten)» — yield * iterator переключит вызываемый код на итерацию по iterator, а когда iterator закончится, вызывающий код вернётся в оригинальный генератор.

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

                        Пример с генераторами:

                        function * gen1(collection, limit = NaN) {
                            let counter = 0;
                            for ( let item of collection ) {
                                yield `item is ${ item }`;
                        
                                if ( ++counter >= limit ) break;
                            }
                        }
                        
                        
                        function * gen2(...collections) {
                            for ( let collection of collections ) {
                                yield * collection;
                        
                                if ( (yield 'end collection') == true ) break;
                            }
                        }
                        
                        let test = gen2( gen1([1, 2, 3, 4, 5, 6, 7], 3), [5, 6, 7], gen1(['a', 'b', 'c']), new Array(999).join('1|').split('|') );
                        let counter = 0, sendValue = void 0;
                        
                        do {
                            let {value, done} = test.next(sendValue);
                        
                            if ( value == 'end collection' ) {
                                if ( ++counter > 2 ) sendValue = true;
                            }
                            else console.log(value);
                        }
                        while( done != true );
                        

                        Вживую
                          0
                          Допустил опечатку в коде: done должен быть объявлен вне цикла do-while.
                          Полностью правильный код
                          function * gen1(collection, limit = NaN) {
                              let counter = 0;
                              for ( let item of collection ) {
                                  yield `item is ${ item }`;
                          
                                  if ( ++counter >= limit ) break;
                              }
                          }
                          
                          
                          function * gen2(...collections) {
                              for ( let collection of collections ) {
                                  yield * collection;
                          
                                  if ( (yield 'end collection') == true ) break;
                              }
                          }
                          
                          let test = gen2( gen1([1, 2, 3, 4, 5, 6, 7], 3), [5, 6, 7], gen1(['a', 'b', 'c']), new Array(999).join('1|').split('|') );
                          let counter = 0, sendValue = void 0;
                          let value, done;
                          
                          do {
                              ({value, done}) = test.next(sendValue);
                          
                              if ( value == 'end collection' ) {
                                  if ( ++counter > 2 ) sendValue = true;
                              }
                              else console.log(value);
                          }
                          while( done != true );
                          


                          Код работающий в FireFox
                          function * gen1(collection, limit = NaN) {
                              let counter = 0;
                              for ( let item of collection ) {
                                  yield 'item is ' + item;
                          
                                  if ( ++counter >= limit ) break;
                              }
                          }
                          
                          
                          function * gen2(...collections) {
                              for ( let collection of collections ) {
                                  yield * collection;
                          
                                  if ( (yield 'end collection') == true ) break;
                              }
                          }
                          
                          let test = gen2( gen1([1, 2, 3, 4, 5, 6, 7], 3), [5, 6, 7], gen1(['a', 'b', 'c']), new Array(999).join('1|').split('|') );
                          let counter = 0, sendValue = void 0;
                          let value, done;
                          
                          do {
                              ({value, done}) = test.next(sendValue);
                          
                              if ( value == 'end collection' ) {
                                  if ( ++counter > 2 ) sendValue = true;
                              }
                              else console.log(value);
                          }
                          while( done != true );
                          

                            +1
                            Мы с вами в одинаковом положении потому что я с генераторами очень мало знаком пока :)
                            Подозреваю что многое из этого всего можно сделать на генераторах, да.
                              +1
                              Насколько понимаю я, трансдьюсеры — это лишь концепт для формализации композиции функций к цепочкам элементов… Причем чисто функциональный. А генераторы по идее больше перекликаются с FRP (RxJS, Kefir). И разница между ними в том, что генератор нужно просить вернуть следующее значение (poll), а Rx уведомляет о новом доступном элементе (push).
                                0
                                > а Rx уведомляет о новом доступном элементе (push).
                                А можно пример, как использовать возможности push в трансдьюсерах?
                                Просто примеры в статье, например
                                reduce([1, 2, 3, 4, 5, 6, 7], first5T(append), []);    // => [1, 2, 3, 4, 5]
                                

                                демонстрируют законченные операции над множеством.
                                  +1
                                  Как-то так

                                  function Stream(transducer, subscriber) {
                                    this._step = transducer(function(result, item) {
                                      if (arguments.length === 2) {  subscriber(item)  }
                                    });
                                  }
                                  
                                  Stream.prototype.push = function(x) {
                                    this._step(null, x);
                                  }
                                  
                                  stream = new Stream(first5T, function(x) {  console.log('>>', x)  });
                                  stream.push(1);
                                  stream.push(2);
                                  stream.push(3);
                                  stream.push(4);
                                  stream.push(5);
                                  stream.push(6);
                                  stream.push(7);
                                  
                            0
                            Пример с потоками навел на мысль… Надо обязательно уточнить, что step() без параметров можно вызывать только один раз — в ответ на аналогичный вызов. Если разрешить обратное, то область применения трансдьюсеров сильно сократится.

                            Для списков трансдьюсер flatten является обратным к partition — что логично. Но если разрешить трансдьюсеру создавать любое число новых коллекций, то это свойство перестанет выполняться для коллекци общего вида, ведь partition будет знать, как создать произвольную коллекцию благодаря step() — а flatten не будет знать, как ее итерировать!

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

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

                            PS Вот и ответ на вопрос, неоднократно звучавший в комментариях — в чем отличие трансдьюсеров и итераторов по части возможностей. Трансдьюсеры, в отличие от итераторов, абстрагируют только от внешнего уровня коллекции — а итераторы, в отличие от трансдьюсеров, гробят тип возвращаемого значения и не могут быть применены к потокам.
                              0
                              Да, step() без параметров вызывается только 1 раз — вначале. Он вызывается во внешенм коде, сами трансдьюсеры не должны его вызывать (только если их самих вызвали без параметров).

                              Тоже самое и для step() с одним параметром, его можно, и нужно, вызывать только если ваш step вызвали с одним параметром.
                                0
                                Тоже самое и для step() с одним параметром, его можно, и нужно, вызывать только если ваш step вызвали с одним параметром.
                                Стоп. А как же раннее прерывание?

                                Походу, все наши примеры использования Reduced были некорректными. return new Reduced(result) или return new Reduced(step(result, item)) писать нельзя, надо писать return new Reduced(step(result)) и return new Reduced(step(step(result, item))) соответственно.

                                Кстати! А зачем нам вообще создавать Reduced самим? Пусть он создается «самым глубоким» трансдьюсером при вызове с одним аргументом. Это решит проблему своего Reduced в каждой библиотеке.
                                  0
                                  При преждевременном завершении обычного завершения уже не будет.
                                    0
                                    В том-то и проблема. Рассмотрим такой пример: jsfiddle.net/mayorovp/4rj5tk64/1/

                                    Там реализована функция reduceList, которая применяет набор трансдьюсеров к заданному списку, а так же два «образцово-показательных» трансдьюсера: take и append.

                                    Ну а дальше выясняется, что друг с другом они работать не хотят, не смотря на полное соблюдение протокола:
                                    console.log(reduceList([1,2,3,4,5], take(3))); // [1,2,3]
                                    console.log(reduceList([1,2,3], append(6,7))); // [1,2,3,6,7]
                                    console.log(reduceList([1,2,3,4,5], take(3), append(6,7))); // [1,2,3] — Может, мы перепутали аргументы местами?..
                                    console.log(reduceList([1,2,3,4,5], append(6,7), take(3))); // [1,2,3] — WTF?!
                                    Что произошло? Take досрочно прервал цикл — и потому не append не дождался конца коллекции.

                                    Теперь веселье номер два:
                                    console.log(reduceList([1,2,3,4,5], append(), take(3))); // [1,2,3]
                                    console.log(reduceList([1,2,3,4], append(5), take(3))); // [1,2,3]
                                    console.log(reduceList([1,2,3], append(4,5), take(3))); // [1,2,3]
                                    console.log(reduceList([1,2], append(3,4,5), take(3))); // Uncaught TypeError: undefined is not a function — WTF?!
                                    Что произошло? Take прервал цикл — но append этого не понял. Получается, что любые трансдьюсеры, которые вставляют данные в середину потока, обязаны тоже уметь обрабатывать досрочное окончание этого самого потока. Что-то мне от протокола плохо…
                                      0
                                      В первом примере всё правильно. Если стоит take(3), то должно вернуться 3 элемента не больше. Представьте как это было бы если бы мы постаринке делали с временными коллекциями.

                                      По поводу второго есть в посте:
                                      Еще придется добавлять проверку Reduced.isReduced(result) в трансдьюсеры, которые несколько раз вызывают step (прим. flatten).
                                        0
                                        А что делать, если я хочу взять первые три элемента коллекции — и добавить к ним в конец еще два? На том же LINQ collection.Take(3).Concat(new[] { 6,7}) — вполне нормальная операция, притом работающая именно так, как и ожидалось.

                                        Не говорю уже о том, что иногда (в том же FRP) символ конца потока вообще терять нельзя.
                                          0
                                          Да, это, видимо, трасдьюсеры не могут.

                                          Но в FRP ничего не потеряется, там Reduced всплывет, и мы узнаем что нужно закрывать поток.
                                            0
                                            Мы это узнаем только в том случае, если закроем поток явно, в обход всей цепочки трансдьюсеров… Скажем, в вашем же примере методу push понадобится прямой доступ к параметру subscriber, чтобы сказать ему о конце потока — потому что до метода _step символ конца потока трансдьюсер take не допустит.
                                              0
                                              Дописал пример с поддержкой обоих типов завершения, и с начальным вызовом заодно. (не проверял)

                                              function Stream(transducer, onValue, onEnd) {
                                                this._onEnd = onEnd;
                                                this._closed = false;
                                                this._step = transducer(function(result, item) {
                                                  if (arguments.length === 2) {  onValue(item)  }
                                                });
                                                this._step();
                                              }
                                              
                                              Stream.prototype.push = function(x) {
                                                if (!this._closed) {
                                                  if (isReduced(this._step(null, x)) {
                                                    this._closed = true;
                                                    this._onEnd();
                                                  }
                                                }
                                              }
                                              
                                              Stream.prototype.close = function() {
                                                if (!this._closed) {
                                                  this._step(null);
                                                  this._closed = true;
                                                  this._onEnd();
                                                }
                                              }
                                              
                                                0
                                                Вообще подумалось, что step() и step(result) тоже должны иметь право возвращать Reduced, но у Рича это явно не так для step(), и не понятно для step(result).

                                                image

                                                Наверно нужно уже в исходники кложуры лезть чтобы разобраться.
                                        0
                                        В команде clojure начали делать трансдьюсеры для js, посмотрел как у них. Короче у вас просто `reduceList()` неправильный. Здесь `if` не нужен (нужно всегда вызывать `step(result)`):

                                            if (result instanceof Reduced)
                                                return result.wrapped;
                                            else
                                                return step(result);
                                        


                                        Причем в статье всё было верно. Там `step(result)` вызывается всегда. Уберу update из статьи.
                                          0
                                          Собрал в кучу все функции из поста jsfiddle.net/pvavxc04/, только использовал partition как пример функции добавляющей в конец. Всё работатет.
                                            0
                                            Как и ожидалось, где-то в partition спряталась ошибка :)
                                            jsfiddle.net/pvavxc04/1/ — меняем 5 на 4 в take — и вот результат (точнее, его нет)
                                          0
                                          Это действительно упрощает ситуацию в целом — однако, трансдьюсеры, добавляющие элементы в конец списка, стали еще сложнее: им теперь надо помнить, не был ли поток прерван.
                                            0
                                            Почему, нет. jsfiddle.net/pvavxc04/ — я partition не менял, только добавил обработку «начального вызова».
                                      0
                                      Походу, все наши примеры использования Reduced были некорректными
                                      Наверно вы правы. Спасибо!
                                      Дописал дополнение к статье. Т.е. теперь можно делать take+append и всё будет работать!
                                  –1
                                  Ммм… не совсем понятно, зачем абстрагироваться от коллекций и при этом писать reduce, который работает лишь для массива или объекта, похожего на массив.
                                  Надо бы подумать над этим моментом, а иначе все преимущество вынесенного append (потенциально позволяет собирать другую коллекцию вместо массива) уходит практически в никуда: либо собирай им только массив (скучно!), либо для следующей обработки полученной коллекции придется перегонять ее в массив (скучно вдвойне!).

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

                                  А еще лично я бы не стал безоглядно тащить все из функциональщины.
                                  То, что хорошо выглядит на Хаскеле (в силу особенностей синтаксиса), отвратительно выглядит на джаваскрипте.
                                  Имхо, в джаваскрипте лучше работает коллекция (массив).

                                  То есть для достижения того же результата, достаточно просто остановиться на нотации типа step = function (result, item) {… return {result: new_result, item: new_item};}, договориться о «break» и «continue», т.е. на ряду со специальным значением Reduced, которое в данном случае может выглядеть как {result: new_result} ввести специальное значение Skipped — {result: new_result, skip: true} — которое позволяет перейти на следующий шаг итерации, не заканчивая вычислять результат этой), а потом положить набор таких функций в массив, который дефакто будет трасдьюсером (слово то какое сложное!). Потом на каждом шаге итерации надо просто бежать по этому массиву, вычисляя промежуточный результат. Такая реализация упрощает написание атомарных трансдьюсеров (нет нужды писать функцию, которая возвращает функцию, возвращающую функцию; пишем функцию попроще, которая возвращает функцию типа step).

                                  Пример кода использования для filterMapTake для такой реализации:
                                  transduce([1, 2, 3, 4, 5, 6, 7, 2, 3, 1], [], [
                                    filter(function (x) { return x < 4}),
                                    map(function (x) {return -x},
                                    take(5),
                                    append
                                  ]);
                                  

                                  где transduce = function (coll, seed, transducer)
                                  Имхо, такой синтаксис более оправдан; не вижу особого смысла выделять append и усложнять его специальными случаями.

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

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