Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 671
ayushgp Avatar asked Sep 12 '25 01:09

ayushgp


2 Answers

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])
like image 82
georg Avatar answered Sep 14 '25 14:09

georg


You can achieve this using piping, i dunno if this makes it too complicated, but by using piping you can call Array.reduce on the pipe and it performs the same behaviour on each iteration.

const stream = ["d2", "a2", "b1", "b3", "c"];

const _pipe = (a, b) => (arg) => b(a(arg));
const pipe = (...ops) => ops.reduce(_pipe);

const _map = (value) => (console.log(`map: ${value}`), value.toUpperCase());
const _filter = (value) => (console.log(`filter: ${value}`), 
value.startsWith("A") ? value : undefined);
const _forEach = (value) => value ? (console.log(`forEach: ${value}`), value) : undefined;

const mapFilterEach = pipe(_map,_filter,_forEach);

const result = stream.reduce((sum, element) => {
    const value = mapFilterEach(element);
    if(value) sum.push(value);
    return sum;
}, []);

I took the pipe function from here


Here is a polyfill of the pipe reduce and an example if you want to use it for more dynamic purposes

Array.prototype.pipeReduce = function(...pipes){
    const _pipe = (a, b) => (arg) => b(a(arg));
    const pipe = (...ops) => ops.reduce(_pipe);
    const reducePipes = pipe(...pipes);
    return this.reduce((sum, element) => {
        const value = reducePipes(element);
        if(value) sum.push(value);
        return sum;
    }, []);
};

const stream = ["d2", "a2", "b1", "b3", "c"];

const reduced = stream.pipeReduce((mapValue) => {
    console.log(`map: ${mapValue}`);
    return mapValue.toUpperCase();
}, (filterValue) => {
    console.log(`filter: ${filterValue}`);
    return filterValue.startsWith("A") ? filterValue : undefined;
}, (forEachValue) => {
    if(forEachValue){
        console.log(`forEach: ${forEachValue}`);
        return forEachValue;
    }
    return undefined;
});

console.log(reduced); //["A2"]
like image 31
Pavlo Avatar answered Sep 14 '25 15:09

Pavlo