How to clone ES6 generator?

2019-03-18 14:42发布

问题:

I'm trying to create a List monad in ES6 using generators. To make it work I need to create a copy of an iterator that has already consumed several states. How do I clone an iterator in ES6?

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

var x = test();
console.log(x.next().value); // 1
var y = clone(x);
console.log(x.next().value); // 2
console.log(y.next().value); // 2 (sic)

I've tried clone and cloneDeep from lodash, but they were of no use. Iterators that are returned in this way are native functions and keep their state internally, so it seems there's no way to do it with own JS code.

回答1:

Iterators […] keep their state internally, so it seems there's no way

Yes, and that for a good reason. You cannot clone the state, or otherwise you could tamper too much with the generator.

It might be possible however to create a second iterator that runs alongside of the first one, by memorizing its sequence and yielding it later again. However, there should be only one iterator that really drives the generator - otherwise, which of your clones would be allowed to send next() arguments?



回答2:

I wrote a do-notation library for JavaScript, burrido. To get around the mutable generator problem I made immutagen, which emulates an immutable generator by maintaining a history of input values and replaying them to clone the generator at any particular state.



回答3:

You can't clone a generator--it's just a function with no state. What could have state, and therefore what could be cloned, is the iterator resulting from invoking the generator function.

This approach caches intermediate results, so that cloned iterators can access them if necessary until they "catch up". It returns an object which is both an iterator and an iterable, so you can either call next on it or for...of over it. Any iterator may be passed in, so you could in theory have cloned iterators over an array by passing in array.values(). Whichever clone calls next first at a given point in the iteration will have the argument passed to next, if any, reflected in the value of the yield in the underlying generator.

function clonableIterator(it) {
  var vals = [];

  return function make(n) {
    return {
      next(arg) {
        const len = vals.length;
        if (n >= len) vals[len] = it.next(arg);
        return vals[n++];
      },
      clone()   { return make(n); },
      throw(e)  { if (it.throw) it.throw(e); },
      return(v) { if (it.return) it.return(v); },
      [Symbol.iterator]() { return this; }
    };
  }(0);
}

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

var it = clonableIterator(gen());

console.log(it.next());
var clone = it.clone();
console.log(clone.next());
console.log(it.next());

Obviously this approach has the problem that it keeps the entire history of the iterator. One optimization would be to keep a WeakMap of all the cloned iterators and how far they have progressed, and then clean up the history to eliminate all the past values that have already been consumed by all clones.