javascriptfunctional-programmingmonads

How to reduce iterations when chaining map reduce filter?


I have been reading about map, reduce and filter a lot because of how much they are used in react and FP in general. If we write something like:

let myArr = [1,2,3,4,5,6,7,8,9]
let sumOfDoubleOfOddNumbers = myArr.filter(num => num % 2)
                                   .map(num => num * 2)
                                   .reduce((acc, currVal) => acc + currVal, 0);

3 different loops are run.

I've read about Java 8 streams as well and know that they use what is called a monad, ie, the computations are stored first. They are performed once only in one iteration. For example,

Stream.of("d2", "a2", "b1", "b3", "c")
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase();
    })
    .filter(s -> {
        System.out.println("filter: " + s);
        return s.startsWith("A");
    })
    .forEach(s -> System.out.println("forEach: " + s));

// map:     d2
// filter:  D2
// map:     a2
// filter:  A2
// forEach: A2
// map:     b1
// filter:  B1
// map:     b3
// filter:  B3
// map:     c
// filter:  C

PS: Java code is taken from: http://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/

There are many other languages that use this same method. Is there a way to do it the same way in JS as well?


Solution

  • This is an exact clone of your Java code. Unlike Bergi's solution, no need to modify global prototypes.

    class Stream {
        constructor(iter) {
            this.iter = iter;
        }
    
        * [Symbol.iterator]() {
            yield* this.iter;
        }
    
        static of(...args) {
            return new this(function* () {
                yield* args
            }());
        }
    
        _chain(next) {
            return new this.constructor(next.call(this));
        }
    
        map(fn) {
            return this._chain(function* () {
                for (let a of this)
                    yield fn(a);
            });
        }
    
        filter(fn) {
            return this._chain(function* () {
                for (let a of this)
                    if (fn(a))
                        yield (a);
            });
        }
    
        forEach(fn) {
            for (let a of this)
                fn(a)
        }
    }
    
    
    Stream.of("d2", "a2", "b1", "b3", "c")
        .map(s => {
            console.log("map: " + s);
            return s.toUpperCase();
        })
        .filter(s => {
            console.log("filter: " + s);
            return s.startsWith("A");
        })
        .forEach(s => console.log('forEach', s));

    Actually, the chaining functionality could be decoupled from specific iterators to provide a generic framework:

    // polyfill, remove me later on
    Array.prototype.values = Array.prototype.values || function* () { yield* this };
    
    class Iter {
        constructor(iter)     { this.iter = iter }
        * [Symbol.iterator]() { yield* this.iter }
        static of(...args)    { return this.from(args) }
        static from(args)     { return new this(args.values()) }
        _(gen)                { return new this.constructor(gen.call(this)) }
    }
    

    Now, you can throw arbitrary generators into this, both predefined and ad-hoc ones, for example:

    let map = fn => function* () {
        for (let a of this)
            yield fn(a);
    };
    
    let filter = fn => function* () {
        for (let a of this)
            if (fn(a))
                yield (a);
    };
    
    it = Iter.of("d2", "a2", "b1", "b3", "c", "a000")
        ._(map(s => s.toUpperCase()))
        ._(filter(s => s.startsWith("A")))
        ._(function*() {
            for (let x of [...this].sort())
                yield x;
        });
    
    console.log([...it])