Mapping a function on a generator in JavaScript

2019-04-23 11:56发布

I have a generator called generateNumbers in JavaScript and another generator generateLargerNumbers which takes each value generated by generateNumbers and applies a function addOne to it, as such:

function addOne(value) {
  return value + 1
}

function* generateNumbers() {
  yield 1
  yield 2
  yield 3
}

function* generateLargerNumbers() {
  for (const number of generateNumbers()) {
    yield addOne(number)
  }
}

Is there any terser way to do this without building an array out of the generated values? I'm thinking something like:

function* generateLargerNumbers() {
  yield* generateNumbers().map(addOne) // obviously doesn't work
}

4条回答
一夜七次
2楼-- · 2019-04-23 12:23

There isn't a built-in way to map over Generator objects, but you could roll your own function:

const Generator = Object.getPrototypeOf(function* () {});
Generator.prototype.map = function* (mapper, thisArg) {
  for (const val of this) {
    yield mapper.call(thisArg, val);
  }
};

Now you can do:

function generateLargerNumbers() {
  return generateNumbers().map(addOne);
}

const Generator = Object.getPrototypeOf(function* () {});
Generator.prototype.map = function* (mapper, thisArg) {
  for (const val of this) {
    yield mapper.call(thisArg, val);
  }
};

function addOne(value) {
  return value + 1
}

function* generateNumbers() {
  yield 1
  yield 2
  yield 3
}

function generateLargerNumbers() {
  return generateNumbers().map(addOne)
}

console.log(...generateLargerNumbers())

查看更多
霸刀☆藐视天下
3楼-- · 2019-04-23 12:25

If you need to actually pass values to your generator then you can't do it with for...of, you have to pass each value through

  const mapGenerator = (generatorFunc, mapper) =>
    function*(...args) {
      let gen = generatorFunc(...args),
        i = 0,
        value;
      while (true) {
        const it = gen.next(value);
        if (it.done) return mapper(it.value, i);
        value = yield mapper(it.value, i);
        i++;
      }
    };

function* generator() {
  console.log('generator received', yield 1);
  console.log('generator received', yield 2);
  console.log('generator received', yield 3);
  return 4;
}

const mapGenerator = (generatorFunc, mapper) =>
  function*(...args) {
    let gen = generatorFunc(...args),
      i = 0,
      value;
    while (true) {
      const it = gen.next(value);
      if (it.done) return mapper(it.value, i);
      value = yield mapper(it.value, i);
      i++;
    }
  };

const otherGenerator = mapGenerator(generator, x => x + 1)

const it = otherGenerator();
console.log(
  it.next().value,
  it.next('a').value, 
  it.next('b').value,
  it.next('c').value
);

查看更多
神经病院院长
4楼-- · 2019-04-23 12:44

How about composing an iterator object, instead of using the nested generators?

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

function generateGreaterNumbers(){
 return { next(){ var r = this.gen.next(); r.value+=1; return r; }, gen: generateNumbers() };`
}
查看更多
狗以群分
5楼-- · 2019-04-23 12:48

higher-order generators

You can choose to manipulate the generator functions themselves

const Generator =
  {
    map: (f,g) => function* (...args)
      {
        for (const x of g (...args))
          yield f (x)
      },
    filter: (f,g) => function* (...args)
      {
        for (const x of g (...args))
          if (f (x))
            yield x
      }
  }

// some functions !
const square = x =>
  x * x

const isEven = x =>
  (x & 1) === 0
  
// a generator !
const range = function* (x = 0, y = 1)
  {
    while (x < y)
      yield x++
  }

// higher order generator !
for (const x of Generator.map (square, Generator.filter (isEven, range)) (0,10))
  console.log('evens squared', x)

higher-order iterators

Or you can choose to manipulate iterators

const Iterator =
  {
    map: (f, it) => function* ()
      {
        for (const x of it)
          yield f (x)
      } (),
    filter: (f, it) => function* ()
      {
        for (const x of it)
          if (f (x))
            yield x
      } ()
  }

// some functions !
const square = x =>
  x * x
  
const isEven = x =>
  (x & 1) === 0

// a generator !
const range = function* (x = 0, y = 1)
  {
    while (x < y)
      yield x++
  }
  
// higher-order iterators !
for (const x of Iterator.map (square, Iterator.filter (isEven, range (0, 10))))
  console.log('evens squared', x)

recommendation

In most cases, I think it's more practical to manipulate the iterator because of it's well-defined (albeit kludgy) interface. It allows you to do something like

Iterator.map (square, Iterator.filter (isEven, [10,11,12,13]))

Whereas the other approach is

Generator.map (square, Generator.filter (isEven, Array.from)) ([10,11,12,13])

Both have a use-case, but I find the former much nicer than the latter


persistent iterators

JavaScript's stateful iterators annoy me – each subsequent call to .next alters the internal state irreversibly.

But! there's nothing stopping you from making your own iterators tho and then creating an adapter to plug into JavaScript's stack-safe generator mechanism

If this interests you, you might like some of the other accompanying examples found here: Loop to a filesystem structure in my object to get all the files

The only gain isn't that we can reuse a persistent iterator, it's that with this implementation, subsequent reads are even faster than the first because of memoisation – score: JavaScript 0, Persistent Iterators 2

// -------------------------------------------------------------------
const Memo = (f, memo) => () =>
  memo === undefined
    ? (memo = f (), memo)
    : memo

// -------------------------------------------------------------------
const Yield = (value, next = Return) =>
  ({ done: false, value, next: Memo (next) })
  
const Return = value =>
  ({ done: true, value })
  
// -------------------------------------------------------------------
const MappedIterator = (f, it = Return ()) =>
  it.done
    ? Return ()
    : Yield (f (it.value), () => MappedIterator (f, it.next ()))
    
const FilteredIterator = (f, it = Return ()) =>
  it.done
    ? Return ()
    : f (it.value)
      ? Yield (it.value, () => FilteredIterator (f, it.next ()))
      : FilteredIterator (f, it.next ())

// -------------------------------------------------------------------
const Generator = function* (it = Return ())
  {
    while (it.done === false)
      (yield it.value, it = it.next ())
    return it.value
  }

// -------------------------------------------------------------------  
const Range = (x = 0, y = 1) =>
  x < y
    ? Yield (x, () => Range (x + 1, y))
    : Return ()

const square = x =>
  x * x

const isEven = x =>
  (x & 1) === 0

// -------------------------------------------------------------------
for (const x of Generator (MappedIterator (square, FilteredIterator (isEven, Range (0,10)))))
  console.log ('evens squared', x)

查看更多
登录 后发表回答