Pull to refresh

Comments 56

За лексические и грамматические ошибки во втором предложении.

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

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

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

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

Проблемы начинаются тогда, когда нужно, например, отсортировать коллекцию (или найти максимальный элемент). Для решения этой задачи коллекцию придется вычитать полностью (как и в описанном в статье методе).
Да, похоже. А IEnumerable позволяет заранее создать сложную операцию (flatten+map+filter+take) и потом применить ее сначала к списку, потом её же к хеш-мепу, и её же передать в библиотеку генерирующую значения со временем (Rx и пр)?
Работая с 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 я не знаком. но думаю, что там тоже 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|]
Так и на C# с LINQ можно почти также.
Там в примере выше и был LINQ, но C# не выводит типы, поэтому одна строчка полезного кода обрамлена кучей лабуды с типизацией.
Не знаю, за меня все это решарпер пишет, поэтому вообще не напрягает. А читать так даже удобнее, всегда все понятно.
Меня тоже не напрягло пока писал только на C# и C++, но к хорошему быстро привыкаешь :)
в этом смысле эталонной реализацией являются Rx. Причем одним из самых интересных участков являются указание местоположения подписки (SubscribeOn метод) и наблюдения (ObserveOn метод), а также генераторы (timer, timestamp и т.д.).

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

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

Однако в целом согласен — в языках, где есть интерфейс перечисления, трансдьюсеры будут как пятое колесо в телеге.
С моей точки зрения Трансдьюсеры очень близки к попытке сделать Stream Fusion\deforestation руками. С одной стороны выходит больше контроля чем если это было сделано автоматически компилятором, с другой стороны нужно больше телодвижений и больше шансов для ошибки.
Причем, насколько мне известно, эта попытка не учитывай почти 30-летний опыт в этой области.
Я еще так до конца и не понял что такое монады, а теперь еще надо понять что такое трансдьюсеры:)
UFO just landed and posted this here
Через 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;
    };
}



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

 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 =)
Я, например, использую трансляцию «на лету» (конечно, для продакшена, лучше странслировать всё на этапе сборки проекта):

код:
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, то, по-моему всё слишком переусложнено.
Я бы написал проще:
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 (Наиболее полная поддержка es6, развивается Гуглом)
* esnext (Активно развивается, большое комьюнити, часть кода развивается facebook'ом)
* es6-transpiler (Трансляция строчка-в-строчку, простой и производительный выходной код)
Напоминает хаскелевский fusion, только реализованный вручную.
Это что-то вроде Streams в Java 8?

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

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

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

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

Это лишь мои догадки, я могу быть не прав.
«Трансдьюсеры» это похоже «yield» наоборот: если yield как бы проталкивает элементы коллекции наверх через цепочку преобразований, то «трансдьюсеры» сначала находят композицию преобразований и применяют эту композицию к коллекции. Учитывая хитровывернутость «трансдьюсеров» и простоту yield, полезность первых сомнительна в языках где есть этот yield и JS в числе таких языков (yield появился в ES6 и уже реализован много где, в том числе и в ноде).
Было бы интересно глянуть на похожие фичи реализованные через yield. Я yield люблю (как правило пишу на python) но с ходу похожего поведения не вижу.
В случае с yield задачу можно решить так: исходный массив обернуть в итератор на основе которого построить другой итератор который перебирает отфильтрованные значения и затем итератор преобразовать в массив. Впрочем вы и сами знаете как этот yield работает. Трансдьюсеры же, судя по всему, комбинируют несколько итераторов в один который и конструирует отфильтрованную коллекцию.

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

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


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

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» гибче, чем простая последовательность операторов.
Sign up to leave a comment.

Articles