Live data from Hacker News

Transducers for JavaScript

github.com

21–27 of 27 posts

Re: Transducers for JavaScript

#21
When I see concepts like transducers I'm always torn in two:

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 

Re: Transducers for JavaScript

#22
post #21

When I see concepts like transducers I'm always torn in two: 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 incomprehensibl…

I agree. They are quite incomprehensible. I looked at the Ruby version https://github.com/cognitect-labs/transducers-ruby because I'm better at Ruby than at JS and Ruby is usually more readable than JS. No way to understand it. I had to dig into the documentation and learn how it works instead of figuring it out from the way it looks.

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.

Re: Transducers for JavaScript

#23
post #21

When I see concepts like transducers I'm always torn in two: 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 incomprehensibl…

"comprehensible" is a relative thing - the traditional functional style you've written was quite mysterious to many developers for some time. I suspect as people try out, understand, and communicate the benefits of transducers benefits - they too will become "comprehensible" :)

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.

Re: Transducers for JavaScript

#24
post #21

When I see concepts like transducers I'm always torn in two: 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 incomprehensibl…

"comprehensible" is a relative thing - the traditional functional style you've written was quite mysterious to many developers for some time. I suspect as people try out, understand, and communicate the benefits of transducers benefits - they too will become "comprehensible" :) 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 wit…

> "comprehensible" is a relative thing

I agree and I hope my pragmatic side will be disproved soon :)

Re: Transducers for JavaScript

#25
post #21

When I see concepts like transducers I'm always torn in two: 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 incomprehensibl…

Yes, the former is more efficient and the latter more comprehensible. But there is a simple step which can reconcile both approaches.

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; };

Re: Transducers for JavaScript

#26
post #25
post #21

When I see concepts like transducers I'm always torn in two: 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 incomprehensibl…

Yes, the former is more efficient and the latter more comprehensible. But there is a simple step which can reconcile both approaches. 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 re…

The code can be simplified, removing all stuff related to null. Initialise the transducer field of reducibles with the identity transformation !

Re: Transducers for JavaScript

#27

It'll be interesting to look into how the Clojure people implemented this. I released the same library a few weeks ago: https://github.com/jlongster/transducers.js , and I'm about to release the next version with an API almost the same as the OP. I think mine has more integration with JS data structures and stuff like monkeypatching existing ones (like immutable-js) to work with this. We'll see how all this shakes ou…

The concept is simple enough and the performance benefits common enough that it would certainly help to converge and contribute the core to one repo with compatibility and other features in supplementary optional repos ... imho.
Post reply on HN