Комментарии 56
Awesome! I waited this article =)
За что минусим?
За лексические и грамматические ошибки во втором предложении.
I have been waiting for an article like this.
I have been waiting for an article like this.
Stack Overflow намного дружелюбней сообщество… Такое ощущение, что тут собрались одни лингвисты и учителя… Не больше единства в славянском народе.
Трансдьюсеры — это попытка переосмыслить операции над коллекциями, такие как map(), filter() и пр., найти в них общую идею, и научиться совмещать вместе несколько операций для дальнейшего переиспользования.thedeemon.livejournal.com/87320.html
Может быть я не до конца понял идею. Но это же IEnumerable? Нет? По крайней мере решаются теже самые проблемы.
Я не знаю точно что такое IEnumerable. Если правильно понимаю, это интерфейс который должны реализовывать коллекции. Если так, то эти вещи стоят рядом но никак друг друга не заменяют. Т.е. вы можете обрабатывать с помощью трансдьюсеров всё, что реализует IEnumerable, но это же не значит что трансдьюсеры == IEnumerable.
Или я неправильно понял?
Или я неправильно понял?
Суть IEnumerable в том, что IEnumerable имеет только один метод — GetEnumerator, который возвращает итератор коллекции. Итератор в свою очередь имеет только один метод — GetNext. Соответственно при вызове методов Filter, Map и тому подобных метод на самом деле ничего не делает, а просто возвращает новый итератор. И только при вычитывании коллекции (материализации) по цепочке начинают лениво вычитываться элементы начиная с самой первой коллекции.
Грубо говоря вы взяли одну коллекцию, и обернули её в Filter. Потом у итератора который возвращен из filter вызвали GetNext. Он начинает дергать GetNext основной коллекции до тех пор, пока не найдет элемент, удовлетворяющий критерию, после этого возвращает его и прекращает работу до следующего запроса. В принципе так можно работать даже с бесконечными коллекциями.
Эта концепция очень напоминает то, что описано в статье (по крайней мере мне).
Проблемы начинаются тогда, когда нужно, например, отсортировать коллекцию (или найти максимальный элемент). Для решения этой задачи коллекцию придется вычитать полностью (как и в описанном в статье методе).
Грубо говоря вы взяли одну коллекцию, и обернули её в Filter. Потом у итератора который возвращен из filter вызвали GetNext. Он начинает дергать GetNext основной коллекции до тех пор, пока не найдет элемент, удовлетворяющий критерию, после этого возвращает его и прекращает работу до следующего запроса. В принципе так можно работать даже с бесконечными коллекциями.
Эта концепция очень напоминает то, что описано в статье (по крайней мере мне).
Проблемы начинаются тогда, когда нужно, например, отсортировать коллекцию (или найти максимальный элемент). Для решения этой задачи коллекцию придется вычитать полностью (как и в описанном в статье методе).
Работая с 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);
}
}
Могут быть ошибки, писал в блокноте, но примерно так.
Какой ужас, как вы только на этом ООП пишите? :-)
Если серьезно, то есть еще вопросы.
Можно ли имея на руках одну сложную операцию сложить ее с другой сложной операцией?
Уже писал, но повторюсь, будет ли это работать с FRP и прочими асинхронными вещами? Для .NET вроде есть реализация Rx.
Вообще подход с тем что возвращается новая коллекция скорее похож на LazyJs, и если создать сложную операцию в LazyJs, её нельзя будет передать в FRP библиотеку. Поэтому и спрашиваю про FRP.
Если серьезно, то есть еще вопросы.
Можно ли имея на руках одну сложную операцию сложить ее с другой сложной операцией?
Уже писал, но повторюсь, будет ли это работать с FRP и прочими асинхронными вещами? Для .NET вроде есть реализация Rx.
Вообще подход с тем что возвращается новая коллекция скорее похож на LazyJs, и если создать сложную операцию в LazyJs, её нельзя будет передать в FRP библиотеку. Поэтому и спрашиваю про FRP.
Склеить можно. Имея две функции которые преобразуют одну последовательность в другую, можно создать функцию которая вызовет сначала первую, а потом передаст её результат во вторую.
С FRP я не знаком. но думаю, что там тоже IEnumerable используются. Они вообще очень широко используются в .NET.
С FRP я не знаком. но думаю, что там тоже IEnumerable используются. Они вообще очень широко используются в .NET.
Между прочим, сложные выражения вокруг этого самого IEnumerable как раз считаются (среди ООП-программистов) функциональщиной :-)
Ну это просто пример кода на C#, который действительно уныл. В том ж F# все выглядит читаемо, компактно и в то же время доступна вся моща .Net среды.
let MySuperMegaOperation source =
source |> Seq.map(fun x -> x + 1) |> Seq.filter(fun x -> x < 4)
MySuperMegaOperation [|1; 2; 3; 4|]
в этом смысле эталонной реализацией являются Rx. Причем одним из самых интересных участков являются указание местоположения подписки (SubscribeOn метод) и наблюдения (ObserveOn метод), а также генераторы (timer, timestamp и т.д.).
Вообще, весь LINQ основывается на деревьях выражений, причем, для оптимизации дальнейших действий можно создать свой syntax tree rewriter.
В таких случаях (flatten+map+filter+take) для reduce чтение выражения справа-налево проходить.
а если это вообще обернуть в сгенерированный код (aka compiled quiery), так вообще «оптимизирующий компилятор» выйдет :)
Вообще, весь LINQ основывается на деревьях выражений, причем, для оптимизации дальнейших действий можно создать свой syntax tree rewriter.
В таких случаях (flatten+map+filter+take) для reduce чтение выражения справа-налево проходить.
а если это вообще обернуть в сгенерированный код (aka compiled quiery), так вообще «оптимизирующий компилятор» выйдет :)
В .NET IEnumerable — это стандартный интерфейс чего-то, что можно перебрать по одному элементу. C# позволяет привесить к любому IEnumerable внешний метод (трансдьюсер в терминологии статьи), который вернет новый IEnumerable, при переборе которого вы получите take/where/map/skip от оригинального IEnumerable. Без создания промежуточной коллекции, без ограничения на тип элементов, и со строгой типизацией. Что-то похожее реализовано в F# (sequences), и наверняка еще много раз до «изобретения» трансдьюсеров.
Не совсем. Операции Select, Where и т.п., примененные к коллекеции, вернут новое перечисление — которое уже не получится так просто привести обратно к типу исходной коллекции. Кроме того, эти операции не позволят использовать некоторые оптимизации, доступные для исходной коллекции — к примеру, операция Take для односвязных списков может быть реализована куда проще, чем реализуется в IEnumerable.
Однако в целом согласен — в языках, где есть интерфейс перечисления, трансдьюсеры будут как пятое колесо в телеге.
Однако в целом согласен — в языках, где есть интерфейс перечисления, трансдьюсеры будут как пятое колесо в телеге.
С моей точки зрения Трансдьюсеры очень близки к попытке сделать Stream Fusion\deforestation руками. С одной стороны выходит больше контроля чем если это было сделано автоматически компилятором, с другой стороны нужно больше телодвижений и больше шансов для ошибки.
Причем, насколько мне известно, эта попытка не учитывай почти 30-летний опыт в этой области.
Причем, насколько мне известно, эта попытка не учитывай почти 30-летний опыт в этой области.
Я еще так до конца и не понял что такое монады, а теперь еще надо понять что такое трансдьюсеры:)
Через 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;
};
}
Или более громоздкие, но асинхронные пайпы из ноды:
upd. Но мой пример не исключает полезность и интересность статьи =) Автору спасибо =)
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. Но мой пример не исключает полезность и интересность статьи =) Автору спасибо =)
А как это вы заюзали стрелочные функции в ноде? Вроде как их только только сделали в v8 и та версия v8 в ноду ещё не попала.
Стрелочные функции в ноде еще не юзал.
CJS + Browserify + Traceur =)
CJS + Browserify + Traceur =)
Я, например, использую трансляцию «на лету» (конечно, для продакшена, лучше странслировать всё на этапе сборки проекта):
код:
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();
Спасибо за статью. Интересно будет почитать следующую часть про _.take.
Но если рассматривать без возможных оптимизаций _.take, то, по-моему всё слишком переусложнено.
Я бы написал проще:
Конечно, мой способ решает 1ю и 4ю проблемы лишь частично, т.к. результат Array Comprehension — это всегда массив, а если coll будет не массивом, то для прохода по нему объект должен реализовывать Iterator Protocol. Но во-первых, возвращение объекта из массивной операции, я считаю неправильным, а во-вторых, для того, чтобы работать с кастомными коллекциями, можно использовать простой цикл for-of, который тоже работает с Iterator Protocol.
Для ленивой работы, можно преобразовать Array Comprehension в Generator Comprehension:
Опять же, мой способ имеет достоинства и недостатки. Достоинства это то, что используются стандартные средства языка, и четко объявляется результат операции — массив или генератор (в случае же использования цикла for-of код тоже тривиален). Существенным недостатком, конечно, является то, что нету простой возможности реализовать что-то типа оптимизации вызова функции _.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.
Красиво, но увы с ECMAScript 6 в браузерах пока плоховато.
Да, но можно пока использовать Traceur от Google
Можно, а иногда и нужно (чтобы не ловить браузерные баги) использовать трансляторы.
Сейчас есть целых три на выбор (каждый со своими достоинствами и недостатками):
* Traceur от Google (Наиболее полная поддержка es6, развивается Гуглом)
* esnext (Активно развивается, большое комьюнити, часть кода развивается facebook'ом)
* es6-transpiler (Трансляция строчка-в-строчку, простой и производительный выходной код)
Сейчас есть целых три на выбор (каждый со своими достоинствами и недостатками):
* Traceur от Google (Наиболее полная поддержка es6, развивается Гуглом)
* esnext (Активно развивается, большое комьюнити, часть кода развивается facebook'ом)
* es6-transpiler (Трансляция строчка-в-строчку, простой и производительный выходной код)
Напоминает хаскелевский fusion, только реализованный вручную.
было уже же, не? или я идею не уловил?
danieltao.com/lazy.js/
danieltao.com/lazy.js/
tl;dr
Это что-то вроде Streams в Java 8?
Это что-то вроде Streams в Java 8?
Это что-то вроде Streams в Java 8?
Насколько я понял, не совсем, но идея очень похожая. Стримы в Java и IEnumerable очень похожи, выше по треду обсуждается изоморфизм идей.
С моей точки зрения, разница лишь в точке отсчёта: со стримами мы отталкиваемся от данных, каждое преобразование порождает новый поток данных из старого. «Трансдьюсеры» же отталкиваются от операций над данными, посволяя компоновать сложные операции свёрток из простых.
У обоих подходов есть недостатки и преимущества. «Трансдьюсеры», насколько я понимаю, не требуют, чтобы объект, по которому происходит свёртка, реализовывал интерфейс вроде IEnumerable. Т.е. если автор типа не предусмотрел интеграции с фрэймворком X, эту интеграцию можно будет добавить позднее, скорее всего, не меняя уже написанного кода и не заворачивая типы в обёртки-адаптеры. Вероятно, они могут быть эффективней, т.к. в теории порождают меньше промежуточных данных.
Расплата за это — инверсия управления. Пользователь потоков сам решает, когда и сколько (и из каких потоков) читать. Пользователь «трансдьюсера» вызывает всегда вызывает свёртку по всей коллекции, что иногда не очень удобно.
Это лишь мои догадки, я могу быть не прав.
«Трансдьюсеры» это похоже «yield» наоборот: если yield как бы проталкивает элементы коллекции наверх через цепочку преобразований, то «трансдьюсеры» сначала находят композицию преобразований и применяют эту композицию к коллекции. Учитывая хитровывернутость «трансдьюсеров» и простоту yield, полезность первых сомнительна в языках где есть этот yield и JS в числе таких языков (yield появился в ES6 и уже реализован много где, в том числе и в ноде).
Было бы интересно глянуть на похожие фичи реализованные через yield. Я yield люблю (как правило пишу на python) но с ходу похожего поведения не вижу.
habrahabr.ru/post/237613/#comment_7992463
Второй блок кода с Generator Comprehension — тут неявный yield
Второй блок кода с Generator Comprehension — тут неявный yield
В случае с yield задачу можно решить так: исходный массив обернуть в итератор на основе которого построить другой итератор который перебирает отфильтрованные значения и затем итератор преобразовать в массив. Впрочем вы и сами знаете как этот yield работает. Трансдьюсеры же, судя по всему, комбинируют несколько итераторов в один который и конструирует отфильтрованную коллекцию.
Спасибо за статью и за ссылки. В презентации Рича очень хорошо всё рассказано.
Мне как-то приходила мысль, что после абстрагирования функции, которая передаётся в filter/map, есть какой-то ещё шаг, который заключается в абстрагировании от самого итерационного процесса. Тогда мне показалось это овер-инжинирингом и я отбросил эту идею. Сейчас же, когда filter/map появляется в таком количестве разных контекстов, становится очевидно, что это следующий разумный шаг в поднятии абстракции.
Например, как вы подметили в статье (и в презентации тоже был про это слайд), что FRP реализует свои filter/map и весь остальной зоопарк, но сущность их не меняется. Например в JS мы уже имеем приличный utility-belt в виде LoDash, как было бы славно их просто применять к FRP или к другим контекстам.
О reduce я когда-то написал статью, там упоминается тот факт, что другие функции высшего порядка сводятся к reduce. Думаю, кто-нибудь найдёт эту статью интересной.
Мне как-то приходила мысль, что после абстрагирования функции, которая передаётся в filter/map, есть какой-то ещё шаг, который заключается в абстрагировании от самого итерационного процесса. Тогда мне показалось это овер-инжинирингом и я отбросил эту идею. Сейчас же, когда filter/map появляется в таком количестве разных контекстов, становится очевидно, что это следующий разумный шаг в поднятии абстракции.
Например, как вы подметили в статье (и в презентации тоже был про это слайд), что FRP реализует свои filter/map и весь остальной зоопарк, но сущность их не меняется. Например в JS мы уже имеем приличный utility-belt в виде LoDash, как было бы славно их просто применять к FRP или к другим контекстам.
Скрытый текст
Ещё мне подумалось, что можно прокачать reduce, так, чтобы он сам скармливал трансдьюсеру верный step и устанавливал сам себе верный seed. Также, вероятно, эта функция будет разруливать ранний останов.
О reduce я когда-то написал статью, там упоминается тот факт, что другие функции высшего порядка сводятся к reduce. Думаю, кто-нибудь найдёт эту статью интересной.
Блин, нонхуман лексикон детектед. Слабо было перевести слово «трансдьюсер» на русский язык? Это же «преобразователь».
Автор, у вас в коде, похоже, небольшая ошибка:
По-моему надо в обратном порядке:
var addOne_lessTnan4 = function(step) { return lessTnan4T(addOneT(step)); } // или, что вообще замечательно, можно использовать функцию _.compose var addOne_lessTnan4 = _.compose(addOneT, lessTnan4T);
По-моему надо в обратном порядке:
var addOne_lessTnan4 = _.compose(lessTnan4T, addOneT);
Проверил, вроде всё правильно.
Попробуйте так:
Эти два трансдьюсера не идентичны.
Кстати, странновато работает первый вариант…
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]
Эти два трансдьюсера не идентичны.
Кстати, странновато работает первый вариант…
compose композит справа налево, но при работе такой композиции она как бы ещё раз перевернётся, и всё станет ок.
Жаль, что в JS нет ленивых списков… Концепция трансдьюсеров по сравнению с концепцией ленивых списков имеет, на мой взгляд, сильный привкус «continuation passing». В прочем, стиль «continuation passing» гибче, чем простая последовательность операторов.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий
Трансдьюсеры в JavaScript. Часть первая