Пользователь
0,0
рейтинг
22 сентября 2014 в 19:02

Разработка → Трансдьюсеры в JavaScript. Часть первая

Рич Хикки, автор языка Clojure, недавно придумал новую концепцию — Трансдьюсеры. Их сразу добавили в Clojure, но сама идея универсальна и может быть воспроизведена в других языках.

Сразу, зачем это нужно:

  • трансдьюсеры могут улучшить производительность, т.к. позволят не создавать временные коллекции в цепочках операций map.filter.takeWhile.etc
  • могут помочь переиспользовать код
  • могут помочь интегрировать библиотеки между собой, например underscore/LoDash могут уметь создавать трансдьюсеры, а FRP библиотеки (RxJS/Bacon.js/Kefir.js) могут уметь их принимать
  • могут упростить FRP библиотеки, т.к. можно будет выбросить кучу методов, добавив один метод для поддержки трансдьюсеров


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



Мы уже умеем совмещать несколько операций:

  function mapFilterTake(coll) {
    return _.take(_.filter(_.map(coll, mapFn), filterFn), 5);
  }

  // (я буду использовать в примерах методы из underscore.js)


Но здесь есть ряд проблем:

  • mapFilterTake() может работать только с определенным типом коллекций
  • его нельзя использовать в ленивом стиле
  • это будет работать медленно с большими коллекциями, ведь на каждом шаге создается временная большая коллекция, и, так как в конце .take(5), бОльшая часть работы вообще будет делаться впустую
  • мы не можем ипользовать mapFilterTake() в FRP/CSP библиотеках


Чтобы объяснить идею трансдьюсеров нужно начать с операции reduce. Если подумать, любая операция над коллекциями может быть выражена через reduce. Начнем с операции map.

function append(coll, item) {
  return coll.concat([item]);
}

var newColl = _.reduce(coll, function(result, item) {
  return append(result, mapFn(item));
}, []);

// аналогичный код через map
var newColl = _.map(coll, mapFn);


Мы начинаем с пустого массива, и добавляем в него результаты: каждый элемент исходного массива пропускаем через функцию mapFn, и добавляем результат в массив result.

Я добавил еще служебную функцию append(), которая просто оборачивает .concat(), дальше станет понятно зачем это нужно.

Теперь выразим filter через reduce.

var newColl = _.reduce(coll, function(result, item) {
  if (filterFn(item)) {
    return append(result, item);
  } else {
    return result;
  }
}, []);

// аналогичный код через filter
var newColl = _.filter(coll, filterFn);


Надеюсь, что здесь тоже всё понятно.

Дальше следовало бы рассказать про .take(), но с ним всё немного сложнее и я расскажу об этом во второй части статьи, пока разберемся с filter и map.

Давайте теперь внимательно посмотрим на функции которые мы передаем в reduce чтобы имитировать map и filter.

function(result, item) {
  return append(result, mapFn(item));
}

function(result, item) {
  if (filterFn(item)) {
    return append(result, item);
  } else {
    return result;
  }
}


У них одинаковый тип принимаемых и возвращаемых значений, значит мы уже нашли что-то общее у map и filter, и движемся в правильном направлении. Но есть одна проблема, они используют внутри функцию append(), которая умеет работать только с массивами, и как следствие сами эти функции тоже могут работать только с массивами. Давайте вытащим append().

function(step) {
  return function(result, item) {
    return step(result, mapFn(item));
  }
}

function(step) {
  return function(result, item) {
    if (filterFn(item)) {
      return step(result, item);
    } else {
      return result;
    }
  }
}


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

Давайте проверим, что пока всё работает.

var mapT = function(step) {
  return function(result, item) {
    return step(result, mapFn(item));
  }
}

var filterT = function(step) {
  return function(result, item) {
    if (filterFn(item)) {
      return step(result, item);
    } else {
      return result;
    }
  }
}

var newColl = _.reduce(coll, mapT(append), []);
var newColl = _.reduce(coll, filterT(append), []);


Вроде работает :-)
Здесь mapT и filterT означает «трандьюсер мап» и «трандьюсер фильтр».

Перед тем как двигаться дальше, давайте еще напишем функции которые генерируют трансдьюсеры разных типов (пока только map и filter).

function map(fn) {
  return function(step) {
    return function(result, item) {
      return step(result, fn(item));
    }
  }
}

function filter(predicate) {
  return function(step) {
    return function(result, item) {
      if (predicate(item)) {
        return step(result, item);
      } else {
        return result;
      }
    }
  }
}

// теперь можно писать так
var addOneT = map(function(x) {return x + 1});
var lessTnan4T = filter(function(x) {return x < 4});

_.reduce([1, 2, 3, 4], addOneT(append), []);      // => [2, 3, 4, 5]
_.reduce([2, 3, 4, 5], lessTnan4T(append), []);   // => [2, 3]


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

var addOne_lessTnan4 = function(step) {
  return addOneT(lessTnan4T(step));
}

// или, что вообще замечательно, можно использовать функцию _.compose
var addOne_lessTnan4 = _.compose(addOneT, lessTnan4T);

// и, конечно, можно использовать наш новый трансдьюсер
_.reduce([1, 2, 3, 4], addOne_lessTnan4(append), []);    // => [2, 3]


Итак, мы научились объединять функции для работы с коллекциями новым способом, и назвали объекты, которые мы объединяем и получаем в результате объединения трансдьюсерами. Но удалось ли решить проблемы объявленые вначале статьи?

1) mapFilterTake() может работать только с определенным типом коллекций

Наш трандьюсер addOne_lessTnan4 ничего не знает про тип коллекции, которую мы его заставляем обрабатывать.
Мы можем использовать другой тип данных. Чтобы получить на выходе не массив, а например объект,
достаточно заменить функцию append, и начальное значение [].

_.reduce([1, 2, 3, 4], addOne_lessTnan4(function(result, item) {
  result[item] = true;
  return result;
}), {});    // => {2: true, 3: true}


Чтобы изменить тип входных данных, нужно вместо _.reduce() использовать другую функцию, которая умеет перебирать этот тип. Это тоже не сложно сделать.

2) mapFilterTake() нельзя использовать в ленивом стиле

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

3) mapFilterTake() будет работать медленно с большими коллекциями

Очевидно у трансдьюсеров здесь всё схвачено.

4) мы не можем ипользовать mapFilterTake() в FRP/CSP библиотеках

Так как трансдьюсеры не привязанны к типу обрабатываемой коллекции, и не создают промежуточных результотов, их можно использовать даже с такими коллекциями как поток событий или Behaviour/Property. Также их можно использовать и в CSP — подходе похожем на FRP. И потенциально можно будет использовать в чем-то новом, чего еще нет.

Во второй части я расскажу как сделать трансдьюсеры take, takeWhile и пр, и о том, что же нам теперь с этим всем делать в JavaScript сообществе.

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

Ссылки по теме:

blog.cognitect.com/blog/2014/8/6/transducers-are-coming — первое упоминание (если не ошибаюсь)
phuu.net/2014/08/31/csp-and-transducers.html — про CSP и трасдьюсеры в JavaScript
jlongster.com/Transducers.js--A-JavaScript-Library-for-Transformation-of-Data — еще раз про трасдьюсеры в JavaScript и немного про CSP
www.youtube.com/watch?v=6mTbuzafcII — Рич Хикки подробно рассказывает про трасдьюсеры
Роман @Pozadi
карма
36,2
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (56)

  • –23
    Awesome! I waited this article =)
    • –7
      За что минусим?
      • +29
        За лексические и грамматические ошибки во втором предложении.

        I have been waiting for an article like this.
        • –12
          Stack Overflow намного дружелюбней сообщество… Такое ощущение, что тут собрались одни лингвисты и учителя… Не больше единства в славянском народе.
          • +18
            На StackOverflow тоже не особо приветствуют бессмысленные комменты. Кроме того, там не приветствуется общение на языке, отличном от языка ресурса.
          • +3
            вы спросили, я ответил.
  • +9
    Трансдьюсеры — это попытка переосмыслить операции над коллекциями, такие как map(), filter() и пр., найти в них общую идею, и научиться совмещать вместе несколько операций для дальнейшего переиспользования.
    thedeemon.livejournal.com/87320.html
    • 0
      Да да, я читал) Об этом во второй части.
  • +8
    Может быть я не до конца понял идею. Но это же IEnumerable? Нет? По крайней мере решаются теже самые проблемы.
    • 0
      Я не знаю точно что такое IEnumerable. Если правильно понимаю, это интерфейс который должны реализовывать коллекции. Если так, то эти вещи стоят рядом но никак друг друга не заменяют. Т.е. вы можете обрабатывать с помощью трансдьюсеров всё, что реализует IEnumerable, но это же не значит что трансдьюсеры == IEnumerable.

      Или я неправильно понял?
      • +8
        Суть IEnumerable в том, что IEnumerable имеет только один метод — GetEnumerator, который возвращает итератор коллекции. Итератор в свою очередь имеет только один метод — GetNext. Соответственно при вызове методов Filter, Map и тому подобных метод на самом деле ничего не делает, а просто возвращает новый итератор. И только при вычитывании коллекции (материализации) по цепочке начинают лениво вычитываться элементы начиная с самой первой коллекции.

        Грубо говоря вы взяли одну коллекцию, и обернули её в Filter. Потом у итератора который возвращен из filter вызвали GetNext. Он начинает дергать GetNext основной коллекции до тех пор, пока не найдет элемент, удовлетворяющий критерию, после этого возвращает его и прекращает работу до следующего запроса. В принципе так можно работать даже с бесконечными коллекциями.

        Эта концепция очень напоминает то, что описано в статье (по крайней мере мне).

        Проблемы начинаются тогда, когда нужно, например, отсортировать коллекцию (или найти максимальный элемент). Для решения этой задачи коллекцию придется вычитать полностью (как и в описанном в статье методе).
        • 0
          Да, похоже. А IEnumerable позволяет заранее создать сложную операцию (flatten+map+filter+take) и потом применить ее сначала к списку, потом её же к хеш-мепу, и её же передать в библиотеку генерирующую значения со временем (Rx и пр)?
          • +4
            Работая с IEnumerable вы просто преобразуете одну последовательность в другую. Поэтому, на сколько я могу судить да, с точностью до типизации. Склеить несколько операций в одну конечно можно:

            static class MySyperExtension
            {
            	public static IEnumerable<TResult> MySuperMegaOperation(IEnumerable<TSource> source)
            	{
            		return source.Where(...).Aggeregate(...). //тут можно быть что-то еще
            	}
            }
            
            //вызывать потом можно так:
            var result = myCollection.MySuperMegaOperation()
            


            Передать тоже можно:

            public class SomeClass 
            {
            	public void SomeMethod<TSource,TResult>(Func<IEnumerable<TSource>, IEnumerable<TResult>> processor)
            	{
            		var myCollection = new List<TSource>()
            		var result = processor.Invoke(source);
            	}
            }
            


            Могут быть ошибки, писал в блокноте, но примерно так.
            • 0
              Какой ужас, как вы только на этом ООП пишите? :-)

              Если серьезно, то есть еще вопросы.

              Можно ли имея на руках одну сложную операцию сложить ее с другой сложной операцией?

              Уже писал, но повторюсь, будет ли это работать с FRP и прочими асинхронными вещами? Для .NET вроде есть реализация Rx.

              Вообще подход с тем что возвращается новая коллекция скорее похож на LazyJs, и если создать сложную операцию в LazyJs, её нельзя будет передать в FRP библиотеку. Поэтому и спрашиваю про FRP.
              • 0
                Склеить можно. Имея две функции которые преобразуют одну последовательность в другую, можно создать функцию которая вызовет сначала первую, а потом передаст её результат во вторую.

                С FRP я не знаком. но думаю, что там тоже IEnumerable используются. Они вообще очень широко используются в .NET.
              • +5
                Между прочим, сложные выражения вокруг этого самого IEnumerable как раз считаются (среди ООП-программистов) функциональщиной :-)
              • +1
                Ну это просто пример кода на C#, который действительно уныл. В том ж F# все выглядит читаемо, компактно и в то же время доступна вся моща .Net среды.

                let MySuperMegaOperation source =
                    source |> Seq.map(fun x -> x + 1) |> Seq.filter(fun x -> x < 4)
                
                MySuperMegaOperation [|1; 2; 3; 4|]
                
                • +1
                  Так и на C# с LINQ можно почти также.
                  • +1
                    Там в примере выше и был LINQ, но C# не выводит типы, поэтому одна строчка полезного кода обрамлена кучей лабуды с типизацией.
                    • +1
                      Не знаю, за меня все это решарпер пишет, поэтому вообще не напрягает. А читать так даже удобнее, всегда все понятно.
                      • +1
                        Меня тоже не напрягло пока писал только на C# и C++, но к хорошему быстро привыкаешь :)
          • +1
            в этом смысле эталонной реализацией являются Rx. Причем одним из самых интересных участков являются указание местоположения подписки (SubscribeOn метод) и наблюдения (ObserveOn метод), а также генераторы (timer, timestamp и т.д.).

            Вообще, весь LINQ основывается на деревьях выражений, причем, для оптимизации дальнейших действий можно создать свой syntax tree rewriter.
            В таких случаях (flatten+map+filter+take) для reduce чтение выражения справа-налево проходить.

            а если это вообще обернуть в сгенерированный код (aka compiled quiery), так вообще «оптимизирующий компилятор» выйдет :)
            • +1
              *а если это обернуть в сгенерированный код (aka compiled quiery), так вообще «оптимизирующий компилятор» выйдет :)
      • +6
        В .NET IEnumerable — это стандартный интерфейс чего-то, что можно перебрать по одному элементу. C# позволяет привесить к любому IEnumerable внешний метод (трансдьюсер в терминологии статьи), который вернет новый IEnumerable, при переборе которого вы получите take/where/map/skip от оригинального IEnumerable. Без создания промежуточной коллекции, без ограничения на тип элементов, и со строгой типизацией. Что-то похожее реализовано в F# (sequences), и наверняка еще много раз до «изобретения» трансдьюсеров.
    • 0
      Не совсем. Операции Select, Where и т.п., примененные к коллекеции, вернут новое перечисление — которое уже не получится так просто привести обратно к типу исходной коллекции. Кроме того, эти операции не позволят использовать некоторые оптимизации, доступные для исходной коллекции — к примеру, операция Take для односвязных списков может быть реализована куда проще, чем реализуется в IEnumerable.

      Однако в целом согласен — в языках, где есть интерфейс перечисления, трансдьюсеры будут как пятое колесо в телеге.
  • 0
    С моей точки зрения Трансдьюсеры очень близки к попытке сделать Stream Fusion\deforestation руками. С одной стороны выходит больше контроля чем если это было сделано автоматически компилятором, с другой стороны нужно больше телодвижений и больше шансов для ошибки.
    Причем, насколько мне известно, эта попытка не учитывай почти 30-летний опыт в этой области.
  • +14
    Я еще так до конца и не понял что такое монады, а теперь еще надо понять что такое трансдьюсеры:)
    • +2
      «Монада — это моноид в категории эндофункторов» — уже чуть ли не мемом стало.
  • +4
    Через Pipelines можно достичь того же результата, плюс меньше кода и менее запутанно:

    Код
    var addOne    = (ctx, x, next) => next(x + 1),
        lessThan4 = (ctx, x, next) => x < 4 && next(x),
        append    = (ctx, x)  => ctx.push(x);
    
    console.log(test(addOne, lessThan4, append));
    console.log(test(lessThan4, addOne, append));
    
    // test
    function test(...fns) {
        return [1, 2, 3, 4].reduce(runner(fns), [])
    }
    // implementation
    function runner(fns){
        return function(ctx, x){
            var i = -1;
            function next(x){
                if (++i < fns.length)
                    fns[i](ctx, x, next);
            }
            next(x);
            return ctx;
        };
    }
    



    • +1
      Или более громоздкие, но асинхронные пайпы из ноды:

       var through2 = require("through2"),
           addOne, lessThan4, append,
           transStream, input, result;
      
      addOne    = (x, next) => next(x+1);
      
      lessThan4 = (x, next) => x < 4 && next(x);
      
      append    = (ctx) => (x, next) => {ctx.push(x); next();};
      
      transStream = (fn) => through2.obj((hunk, enc, cb) => {
          fn(hunk, cb);
      });
      
      (input = transStream(addOne))
          .pipe(transStream(lessThan4))
          .pipe(transStream(append(result = [])))
          .on("finish", () => console.log('result', result));
      
      [1,2,3,4].forEach((item) => input.write(item));
      


      upd. Но мой пример не исключает полезность и интересность статьи =) Автору спасибо =)
      • 0
        А как это вы заюзали стрелочные функции в ноде? Вроде как их только только сделали в v8 и та версия v8 в ноду ещё не попала.
        • –1
          Стрелочные функции в ноде еще не юзал.
          CJS + Browserify + Traceur =)
        • +1
          Я, например, использую трансляцию «на лету» (конечно, для продакшена, лучше странслировать всё на этапе сборки проекта):

          код:
          var es6transpiler = require('es6-transpiler');
          
          // node_inject_on перехватывает вызовы require и транслирует es6-код в es5
          es6transpiler.node_inject_on(function(fileName){
          	return fileName.endsWith(".es6.js");
          });
          
          require("./main.es6.js");// запускаем программу написанную на es6
          
          es6transpiler.node_inject_off();
          

  • +2
    Спасибо за статью. Интересно будет почитать следующую часть про _.take.
    Но если рассматривать без возможных оптимизаций _.take, то, по-моему всё слишком переусложнено.
    Я бы написал проще:
    let coll = [1, 2, 3, 4];
    let addOne = (x) => x + 1;
    let filterFn = (x) => x < 4;
    coll = [ for (item of coll) if ( filterFn(item = addOne(item)) ) item ];
    

    Конечно, мой способ решает 1ю и 4ю проблемы лишь частично, т.к. результат Array Comprehension — это всегда массив, а если coll будет не массивом, то для прохода по нему объект должен реализовывать Iterator Protocol. Но во-первых, возвращение объекта из массивной операции, я считаю неправильным, а во-вторых, для того, чтобы работать с кастомными коллекциями, можно использовать простой цикл for-of, который тоже работает с Iterator Protocol.

    Для ленивой работы, можно преобразовать Array Comprehension в Generator Comprehension:
    coll = ( for (item of coll) if ( filterFn(item = addOne(item)) ) item );
    
    coll.next();
    


    Опять же, мой способ имеет достоинства и недостатки. Достоинства это то, что используются стандартные средства языка, и четко объявляется результат операции — массив или генератор (в случае же использования цикла for-of код тоже тривиален). Существенным недостатком, конечно, является то, что нету простой возможности реализовать что-то типа оптимизации вызова функции _.take.
    • –1
      Красиво, но увы с ECMAScript 6 в браузерах пока плоховато.
      • –2
        Да, но можно пока использовать Traceur от Google
      • –1
        Можно, а иногда и нужно (чтобы не ловить браузерные баги) использовать трансляторы.
        Сейчас есть целых три на выбор (каждый со своими достоинствами и недостатками):
        * Traceur от Google (Наиболее полная поддержка es6, развивается Гуглом)
        * esnext (Активно развивается, большое комьюнити, часть кода развивается facebook'ом)
        * es6-transpiler (Трансляция строчка-в-строчку, простой и производительный выходной код)
  • +2
    Напоминает хаскелевский fusion, только реализованный вручную.
  • 0
    было уже же, не? или я идею не уловил?
    danieltao.com/lazy.js/
  • –1
    tl;dr
    Это что-то вроде Streams в Java 8?
    • +1
      Это что-то вроде Streams в Java 8?

      Насколько я понял, не совсем, но идея очень похожая. Стримы в Java и IEnumerable очень похожи, выше по треду обсуждается изоморфизм идей.

      С моей точки зрения, разница лишь в точке отсчёта: со стримами мы отталкиваемся от данных, каждое преобразование порождает новый поток данных из старого. «Трансдьюсеры» же отталкиваются от операций над данными, посволяя компоновать сложные операции свёрток из простых.

      У обоих подходов есть недостатки и преимущества. «Трансдьюсеры», насколько я понимаю, не требуют, чтобы объект, по которому происходит свёртка, реализовывал интерфейс вроде IEnumerable. Т.е. если автор типа не предусмотрел интеграции с фрэймворком X, эту интеграцию можно будет добавить позднее, скорее всего, не меняя уже написанного кода и не заворачивая типы в обёртки-адаптеры. Вероятно, они могут быть эффективней, т.к. в теории порождают меньше промежуточных данных.

      Расплата за это — инверсия управления. Пользователь потоков сам решает, когда и сколько (и из каких потоков) читать. Пользователь «трансдьюсера» вызывает всегда вызывает свёртку по всей коллекции, что иногда не очень удобно.

      Это лишь мои догадки, я могу быть не прав.
  • 0
    «Трансдьюсеры» это похоже «yield» наоборот: если yield как бы проталкивает элементы коллекции наверх через цепочку преобразований, то «трансдьюсеры» сначала находят композицию преобразований и применяют эту композицию к коллекции. Учитывая хитровывернутость «трансдьюсеров» и простоту yield, полезность первых сомнительна в языках где есть этот yield и JS в числе таких языков (yield появился в ES6 и уже реализован много где, в том числе и в ноде).
    • 0
      Было бы интересно глянуть на похожие фичи реализованные через yield. Я yield люблю (как правило пишу на python) но с ходу похожего поведения не вижу.
      • 0
        habrahabr.ru/post/237613/#comment_7992463
        Второй блок кода с Generator Comprehension — тут неявный yield
      • 0
        В случае с yield задачу можно решить так: исходный массив обернуть в итератор на основе которого построить другой итератор который перебирает отфильтрованные значения и затем итератор преобразовать в массив. Впрочем вы и сами знаете как этот yield работает. Трансдьюсеры же, судя по всему, комбинируют несколько итераторов в один который и конструирует отфильтрованную коллекцию.

  • +1
    Спасибо за статью и за ссылки. В презентации Рича очень хорошо всё рассказано.
    Мне как-то приходила мысль, что после абстрагирования функции, которая передаётся в filter/map, есть какой-то ещё шаг, который заключается в абстрагировании от самого итерационного процесса. Тогда мне показалось это овер-инжинирингом и я отбросил эту идею. Сейчас же, когда filter/map появляется в таком количестве разных контекстов, становится очевидно, что это следующий разумный шаг в поднятии абстракции.
    Например, как вы подметили в статье (и в презентации тоже был про это слайд), что FRP реализует свои filter/map и весь остальной зоопарк, но сущность их не меняется. Например в JS мы уже имеем приличный utility-belt в виде LoDash, как было бы славно их просто применять к FRP или к другим контекстам.

    Скрытый текст
    Ещё мне подумалось, что можно прокачать reduce, так, чтобы он сам скармливал трансдьюсеру верный step и устанавливал сам себе верный seed. Также, вероятно, эта функция будет разруливать ранний останов.


    О reduce я когда-то написал статью, там упоминается тот факт, что другие функции высшего порядка сводятся к reduce. Думаю, кто-нибудь найдёт эту статью интересной.
    • +1
      У TheDeemon'а (ссылка в комментах выше) показывается, что это теорема из теорката. Всё можно свернуть, а что нельзя свернуть, то нужно развернуть… и свернуть!
      • 0
        В статье много незнакомых слов, но суть уловил.
        А ещё там лёгкий стёб над Ричем и пасхалка в одном из слайдов.
  • +3
    Блин, нонхуман лексикон детектед. Слабо было перевести слово «трансдьюсер» на русский язык? Это же «преобразователь».
    • +1
      Как промежуточный вариант можно бы и более исконно латинское «трансдуктор»;-)
  • 0
    Автор, у вас в коде, похоже, небольшая ошибка:

    var addOne_lessTnan4 = function(step) {
      return lessTnan4T(addOneT(step));
    }
    
    // или, что вообще замечательно, можно использовать функцию _.compose
    var addOne_lessTnan4 = _.compose(addOneT, lessTnan4T);
    

    По-моему надо в обратном порядке:

    var addOne_lessTnan4 = _.compose(lessTnan4T, addOneT);
    
    • 0
      Проверил, вроде всё правильно.
      • 0
        Попробуйте так:
        var addOne_lessTnan4 = function(step) {
          return lessTnan4T(addOneT(step));
        }
        
        var addOne_lessTnan4_composition = _.compose(addOneT, lessTnan4T);
        
        _.reduce([1, 2, 3, 4], addOne_lessTnan4(append), []); // [2, 3, 4]
        _.reduce([1, 2, 3, 4], addOne_lessTnan4_composition(append), []); // [2, 3]
        

        Эти два трансдьюсера не идентичны.

        Кстати, странновато работает первый вариант…
        • 0
          Да, точно. Спасибо!
    • 0
      compose композит справа налево, но при работе такой композиции она как бы ещё раз перевернётся, и всё станет ок.
  • 0
    Жаль, что в JS нет ленивых списков… Концепция трансдьюсеров по сравнению с концепцией ленивых списков имеет, на мой взгляд, сильный привкус «continuation passing». В прочем, стиль «continuation passing» гибче, чем простая последовательность операторов.

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