Filter and map functions for generators

Viewed 805

What is the recommended approach in JavaScript to passing around generators that include filtering + mapping logic?

Somehow, JavaScript generators are missing such fundamental things as filter and map operands, similar to arrays, to be able to create a generator that includes that logic, without having to run the iteration first.

My head-on approach was to implement custom functions that apply the logic:

function * filter(g, cb) {
    let a;
    do {
        a = g.next();
        if (!a.done && cb(a.value)) {
            yield a.value;
        }
    } while (!a.done);
    return a.value;
}

function * map(g, cb) {
    let a;
    do {
        a = g.next();
        if (!a.done) {
            yield cb(a.value);
        }
    } while (!a.done);
    return a.value;
}

But this creates a callback hell. I want to simply chain a generator, like a regular array:

// create a filtered & re-mapped generator, without running it:
const gen = myGenerator().filter(a => a > 0).map(b => ({value: b})); 

// pass generator into a function that will run it:
processGenerator(gen);

Is there a way to extend generators to automatically have access to such basic functions?

As an extra, if somebody wants to weight in on why such fundamental things aren't part of the generators implementation, that'll be awesome! I would think that filtering and mapping are the two most essential things one needs for sequences.

3 Answers

An alternative to your solution would be to use the for...of loop instead of a do...while.

I would also prefer the filter and map functions to consume and produce a generator function like below :

function filter(gen, predicate) {
  return function*() {
    for (let e of gen()) {
       if (predicate(e)) {
         yield e; 
       }
     }
  }
}

function map(gen, fn) {
  return function*() {
    for (let e of gen()) {
      yield fn(e);
    }
  }
}

function generatorWrapper(gen) {
  return {
    call: () => gen(),
    filter: predicate => generatorWrapper(filter(gen, predicate)),
    map: fn => generatorWrapper(map(gen, fn))
  };
}

function* generator() {
  yield 1;
  yield 2;
  yield 3;
}

const it = generatorWrapper(generator)
  .filter(x => x > 1)
  .map(x => x * 2)
  .call();

for (let e of it) {
  console.log(e);
}

I may have figured out a proper solution to this...

I created class Iterable, which extends on this answer:

class Iterable {
    constructor(generator) {
        this[Symbol.iterator] = generator;
    }

    static extend(generator, cc) {
        // cc - optional calling context, when generator is a class method;
        return function () {
            return new Iterable(generator.bind(cc ?? this, ...arguments));
        }
    }
}

Iterable.prototype.filter = function (predicate) {
    let iterable = this;
    return new Iterable(function* () {
        for (let value of iterable)
            if (predicate(value)) {
                yield value;
            }
    });
};

Iterable.prototype.map = function (cb) {
    let iterable = this;
    return new Iterable(function* () {
        for (let value of iterable) {
            yield cb(value);
        }
    });
};

Now we can take an existing generator function, like this:

function* test(value1, value2) {
    yield value1;
    yield value2;
}

and turn it into an extended iterator:

const extTest = Iterable.extend(test);

and then use it in place of the original generator:

const i = extTest(111, 222).filter(f => f > 0).map(m => ({value: m}));

This now works correctly:

const values = [...i];
//=> [ { value: 111 }, { value: 222 } ]

UPDATE

After a bit more research, I got convinced that RXJS / IXJS is the way to go. Those wrap iterators and provide extensions out of the box.

How about a pipelining function that will take the original iterable and yield values through pipelined decorators?

const pipe = function* (iterable, decorators) {
  // First build the pipeline by iterating over the decorators
  // and applying them in sequence.
  for(const decorator of decorators) {
    iterable = decorator(iterable)
  }

  // Then yield the values of the composed iterable.
  for(const value of iterable) {
    yield value;
  }
};

const filter = predicate =>
  function* (iterable) {
    for (const value of iterable) {
      if (predicate(value)) {
        yield value;
      }
    }
  };

const map = cb =>
  function* (iterable) {
    for (const value of iterable) {
      yield cb(value);
    }
  };

const mergeMap = cb =>
  function* (iterable) {
    for (const value of iterable) {
      for (const mapped of cb(value)) {
        yield mapped;
      }
    }
  };

const take = n =>
  function* (iterable) {
    for (const value of iterable) {
      if (!n--) {
        break;
      }
      yield value;
    }
  };

function* test(value1, value2) {
  yield value1;
  yield value2;
}

function* infinite() {
  for (;;) yield Math.random();
}

for (const value of pipe(test(111, 222), [
  filter(f => f > 0),
  map(m => ({ value: m }))
])) {
  console.log(value);
}

for (const value of pipe(infinite(), [
  take(5),
  mergeMap(v => [v, { timesTwo: v * 2 }])
])) {
  console.log(value);
}
.as-console-wrapper {
  max-height: 100% !important;
}

Related