Transducers for JavaScript
github.com
github.com
my theoretical side is really excited: reducing (pun intended) a bunch of different operations to a minimal set of basic, composable operations is an awesome intellectual challenge. I imagine how succinct, reusable and multipurpose could become my code.
my pragmatic side is skeptic: in functional programming it's a little step to end up with incomprehensible code, for me and my coworkers:
var inc = function(n) { return n + 1; };
var isEven = function(n) { return n % 2 == 0; };
var xf = comp(map(inc), filter(isEven));
console.log(into([], xf, [0,1,2,3,4])); // [2,4]
vs var inc = function(n) { return n + 1; };
var isEven = function(n) { return n % 2 == 0; };
[0,1,2,3,4].map(inc).filter(isEven); // [2,4]
The latter is comprehensible by everyone (and no additional library).
Ok you iterate twice but if you really (REALLY) have performance problems, maybe you'd end up with something like this: var input = [0,1,2,3,4];
var output = [];
var x;
for (var i = 0, len = input.length ; i < len ; i++ ) {
x = input[i] + 1; // map
if (x % 2 == 0) ouput.push(x); // filter
}The less efficient equivalent is (1..100).select {|n| n.even?}.map {|n| n * 2}.take(5) which is self evident to anybody with a couple of days of Ruby on his back.
I think tranducers are a good idea with still the wrong syntax. Probably the Ruby way to transducers would be along these lines.
(1..100).transduce.select {|n| n.even?}.map {|n| n * 2}.take(5).run
like we build SQL queries with AREL. Maybe this will never work but a possible implementation is adding transduce to a bunch of classes (in this case: Range), having transduce return an object (a Transduceable?) augmented with transducing equivalents of some methods, letting those methods pick sensible defaults for the composition operation and initial value based on the type of the object on the left. Maybe adding transduce to Enumerable is enough to get all the most important classes covered. Probably we need a further method call to actually run the transducing operation we built, that's why the final .run
This is probably not as general as the transducer ruby gem even if we enhance it by passing the reducer and the initial value as arguments as in transduce(reducer: :push, initial_value: []) but it should cover most of the use cases and anybody would be able to read it. This makes the difference when having to urgently fix bugs in the code somebody else wrote two years before and then left the company.
With regards to perf, transduce is often just applying the transducer stack in a for loop - I suspect in far more cases you can now stick with the declarative functional style without sacrificing perf if you adopt transducers.
I agree and I hope my pragmatic side will be disproved soon :)
For that, we have to add some functions, so we can write :
reduce([0,1,2,3,4]).map(inc).filter(isEven).into(array);
The names `reduce`, `into` and `array` may surely be improved; but they convey the idea well enough.The `reduce` function takes an iterable object and turns it into a reducible object i.e. an object with an `into` method to which will be provided all the stuff required to reduce the content of the former iterable into a new array, a sum or whatever result which can be obtained adding items one after the other into a seed.
Note that the `reduce` function doesn't iterate over its argument. Neither do the `map` and `filter` methods. Along the chain the iterable is simply wrapped with functions and filters to be used latter. All computations occure when an actual reducer is provided through a call to the `into` method. Then the mapping and filtering arguments are used to transform the given reducer into a new specific reducer. (this is why they are called transducers). And then the iterable is reduced using some loop like the one you show.
So transducers can be wrapped to be used like regular filters over collections. The beauty of transducers is that they express efficient transformation chains which do not depend of actual input and output. By the way, it seems to me that the proposed javascript transducers miss the last point : the proposed `into` function takes an implied reducer which is computed after the input. An array is always reduced to an array ! What about reduction into a string or a sum ?
Last remark. Bellow a reducible has to wrap a transducer and the code of `into` has to check if there is actually a transducer (chaining mapping and filtering). This is a bit ugly. I think this is due to an over emphasis on transducers. The code would be simpler if we were transforming reducibles either reducers.
-------
transducers.Reducible = function(coll, reduce, transducer) {
this.coll = coll;
this.reduce = reduce;
this.transducer = transducer;
};
transducers.Reducible.prototype.into = function(xf) {
if (this.transducer == null) {
return this.reduce(xf, xf.init(), this.coll);
} else {
transduced_xf = this.transducer(xf);
return this.reduce(transduced_xf, transduced_xf.init(), this.coll);
}
};
transducers.Reducible.prototype.comp = function(other_transducer) {
if (this.transducer == null) {
return other_transducer;
} else {
return transducers.comp(this.transducer, other_transducer);
}
};
transducers.Reducible.prototype.map = function(f) {
new_transducer = this.comp( transducers.map(f) );
return new transducers.Reducible(this.coll, this.reduce, new_transducer);
};
transducers.Reducible.prototype.filter = function(pred) {
new_transducer = this.comp( transducers.filter(pred) );
return new transducers.Reducible(this.coll, this.reduce, new_transducer);
};
transducers.reduce = function(coll) {
if(transducers.isString(coll)) {
return new transducers.Reducible(coll, transducers.stringReduce, null);
} else if(transducers.isArray(coll)) {
return new transducers.Reducible(coll, transducers.arrayReduce, null);
} else if(transducers.isIterable(coll)) {
return new transducers.Reducible(coll, transducers.iterableReduce, null);
} else if(transducers.isObject(coll)) {
return new transducers.Reducible(coll, transducers.objectReduce, null);
} else {
throw new Error("Cannot reduce instance of " + coll.constructor.name);
}
};
transducers.array = {}
transducers.array.init = function() { return []; };
transducers.array.result = function(result) { return result; };
transducers.array.step = function(result, input) { result.push(input); return result; };
transducers.sum = {}
transducers.sum.init = function() { return 0; };
transducers.sum.result = function(result) { return result; };
transducers.sum.step = function(result, input) { return result + input; };"To me, transducers are a generic and composable way to operate on a collection of values, producing a new value or new collection of new values. The word 'transducer' itself can be split into two parts that reflect this definition: 'transform' — to produce some value from another — and 'reducer' — to combine the values of a data structure to produce a new one."
var f=_.compose( _.partialRight(_.filter,isEven),_.partialRight(_.map,inc)) ;
or something.[1] http://en.wikipedia.org/wiki/Zipper_%28data_structure%29
Whereas lodash operates on arrays and objects calculating intermediate results, transducers simply define the transformation in terms of functions similar to what you pass reduce: start with a memo, execute a function with a memo and an item, return the possibly transformed memo for the next iteration. Once you abstract the transformation away from the data, you can apply the same transformations to different processes that start with an initial value and step through a result. One benefit is that you can compute the result in one pass (without intermediate results). Another is you can use the same transformation in different contexts (lazy lists, indefinite sequence generation, CSP, event streams, etc.).
The source could be anything that produces a sequences of values: streams, iterators, callbacks, immutable-js, etc. You simply have to define (external to the transducer) how you append each item to the supplied result. The "step function" that knows how to append results to values is passed to the transducer, and the transducer executes the step function when reducing over results.
It's interesting you mention lodash in the context of transducers, as I have been developing my own take on transducers closely following the underscore API [1].
On the surface they seem very similar.
as in var evens = [1,2,3,4].where(x => x%2 == 0).toarray()
no ?