Is it possible to reset an ECMAScript 6 generator to its initial state?
Asked Answered
F

11

41

Given the provided (very simple) generator, is it possible to return the generator back to its original state to use again?

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

var iterable = generator();

for (let x of iterable) {
    console.log(x);
}

// At this point, iterable is consumed.
// Is there a method for moving iterable back
// to the start point by only without re-calling generator(),
// (or possibly by re-calling generator(), only by using prototype 
//  or constructor methods available within the iterable object)
// so the following code would work again?

for (let x of iterable) {
    console.log(x);
}

I would like to be able to pass the iterable off to some other scope, iterate over it, do some other stuff, then be able to iterate over it again later on in that same scope.

Fourinhand answered 24/5, 2014 at 17:59 Comment(7)
Not according to the current draft: people.mozilla.org/~jorendorff/…Hyperventilation
Btw, shouldn't for (let x in iterable) be for (let x of iterable) ?Hyperventilation
Yes, you are correct -- I will update the code.Fourinhand
As a note for anyone else reading this, I am currently using the following work around (not necessarily a solution). By creating an @@iterator property on the generator function prototype which automatically executes itself on access, returning the underlying generator's @@iterator, I can essentially loop over the generator function instead. The end result is I can iterate over the generator function as desired, allowing it to automatically create the underlying generator for me. As to why ES6 does not include this functionality by default, I am not sure...Fourinhand
Iterators have state, not generators. You call the generator to give you an iterator, and once it's consumed, you call the generator again to iterate again.Scarberry
Sure. My original desire (years ago) was to be able to iterate over generators similarly to iterating over other iterable objects (arrays, sets, maps, etc) where you could use generators in place with for (let val of iterable) {}. I ended up making my own wrapper class that had Symbol.iterator defined on it, which would automatically expand any wrapped generators (that didn't require any arguments, anyways) before attempting an iteration. It worked fine. Treating generators as lazy loading sequences of data wasn't great as far as performance goes, though.Fourinhand
See also https://mcmap.net/q/88872/-how-to-clone-es6-generatorMillennial
D
30

If your intention is

to some other scope, iterate over it, do some other stuff, then be able to iterate over it again later on in that same scope.

Then the only thing you shouldn't try doing is passing the iterator, instead pass the generator:

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

var user = function(generator){

    for (let x of generator()) {
        console.log(x);
    }

    for (let x of generator()) {
        console.log(x);
    }
}

Or just make a "round robin" iterator and check while iterating

var generator = function*() {
    while(true){
        yield 1;
        yield 2;
        yield 3;
    }
};

for( x in i ){
    console.log(x);
    if(x === 3){
        break;
    }
}
Dillingham answered 24/5, 2014 at 18:33 Comment(2)
Adding the while loop inside the generator was brilliant and just helped me alot. Thank you!!Represent
i love you its work!Outgeneral
M
18

At this point, iterable is consumed.

Which means its internal [[GeneratorState]] is completed.

Is there a method for moving iterable back to the start point by only without re-calling generator()

No. The spec states

Once a generator enters the "completed" state it never leaves it and its associated execution context is never resumed. Any execution state associated with generator can be discard at this point.

or possibly by re-calling generator(), only by using prototype or constructor methods available within the iterable object

No. While not explicitly stated in the spec, there are no more instance-specific properties available on the iterable object than [[GeneratorState]] and [[GeneratorContext]].

However, the informative "Generator Object Relationships" grapic states:

Each Generator Function has an associated prototype that does not have a constructor property. Hence a generator instance does not expose access to its generator function.

I would like to be able to pass the iterable off to some other scope

Pass the generator function instead. Or something that yields new generator instances.

Millennial answered 24/5, 2014 at 18:48 Comment(1)
I think this makes sense given the current spec. Thank you for the quotes and suggestions.Fourinhand
P
7

Whenever you need to "reset" an iterable, just toss the old one away and make a new one.

var generator = function*() {
    yield 1;
    yield 2;
    yield 3;
};
const makeIterable = () => generator()

for (let x of makeIterable()) {
    console.log(x);
}

// At this point, iterable is consumed.
// Is there a method for moving iterable back
// to the start point by only without re-calling generator(),
// (or possibly by re-calling generator(), only by using prototype 
//  or constructor methods available within the iterable object)
// so the following code would work again?

for (let x of makeIterable()) {
    console.log(x);
}
Parse answered 11/7, 2017 at 22:6 Comment(1)
I think this should be the answer.Weltschmerz
B
6
  • You can pass to the .next() method of the generator an optional parameter that you can use to reset the state of the generator.

  • The yield doesn't only produce the state of the current call of the generator's iterable, but also looks for the state passed to the generator.

  • So to answer your question, Yes you can reset the generator to its initial state as long as its not been completed (done is still false). In your case, you'll have to change your generator code to the following:

let generator = function* () {
  let count = 0;
  while (true) {
    count += 1;
    /*
    if you pass a parameter of value `true` to the iterable's next,
    the `yield` will be equal to true.
    Thus `reset` will be true and the if condition below will executed.
    Like that the yielded value `count` will be reset to its initial value of 0.
    */
    let reset = yield count;
    if (reset) {
      count = 0;
    }
  }
}

let iterable = generator();

console.log(iterable.next().value);  // 1
console.log(iterable.next().value);  // 2
console.log(iterable.next().value);  // 3
console.log(iterable.next().value);  // 4
console.log(iterable.next(true).value);  // 1 as you can see the count has been reset
console.log(iterable.next().value);  // 2
console.log(iterable.next().value); // 3

Bouzoun answered 12/6, 2021 at 16:8 Comment(0)
O
5

As best I can tell that isn't possible. Per this useful wiki and the draft version of ES6 on generators, once you've returned from it (rather than yielded), it puts it into the "closed" state and there is no way to move it back to the "newborn" state which is how a new generator starts out.

You may have to pass along a callback to your other scope for creating a new generator. As a work-around, you could even add that callback as a custom method on the generator you sent to the other scope if you wanted and that callback would create a new generator for the other scope.

If you think about how generators work, they'd have to execute over from scratch to reset their initial state and there's simply no reason to support that. That would be analagous to asking why you can't just re-execute the constructor on an existing object and expect to have a virgin object in the same object. While it's all technically doable, it's hairy to make work right and there's really no reason to support it. If you want a virgin object, just create a new one. Same with a generator.


This is a bit of a hack, but a curious thing to contemplate. You could make a generator that repeated itself. Suppose your generator worked like this:

var generator = function*() {
    while (true) {
        yield 1;
        yield 2;
        yield 3;
        yield null;
    }
};

var iterable = generator();

for (let x of iterable) {
    if (x === null) break;
    console.log(x);
}

// generator is now in a state ready to repeat again

I can easily see how this might be an anti-pattern though because if you ever do this:

for (let x of iterable) {
    console.log(x);
}

You will have an infinite loop, so it would have to be used with great care. FYI, the above wiki shows examples of an infinite Fibonacci sequence so an infinite generator is certainly contemplated.

Overhand answered 24/5, 2014 at 18:11 Comment(3)
That is an interesting thought. I don't believe it will work in my situation, though, as I won't have access to modify the original generators (I'm considering extending a library of mine which contains array manipulations to also work with generator manipulations).Fourinhand
@Fourinhand - you could wrap an iterator with your own iterator that has a .restart() method or something like that. Your .restart() method would just create a new iterator from your library and the next call to .next() on your outer iterator would start fresh with the new internal iterator. There are lots of possibilities here to allow the remote scope to start over.Overhand
I'm thinking that's the route I will be taking. Hopefully. Part of the problem I am running into is the ability to chain methods from one generator to the next -- similar to C# Linq, where you could call MyEnumerable.Where(x => x.id > 3).Select(x => x.value); But! That's a question for another day.Fourinhand
C
5

As per the draft version of ES6,

Once a generator enters the "completed" state it never leaves it and its associated execution context is never resumed. Any execution state associated with generator can be discard at this point.

So, there is no way to reset it once it is completed. It also makes sense to be so. We call it a generator, for a reason :)

Chalky answered 24/5, 2014 at 18:13 Comment(2)
Bummer. I suppose I'm barking up C#'s tree (which is what I'm used to) where you can execute foreach loops on the same IEnumerable without re-initializing it. I suppose that is internally calling GetEnumerator() which is essentially re-creating the iterator.Fourinhand
Let's say the pointer is on the 4th of six items. It' not completed yet right..? I suppose it is possible then? ;)Elexa
S
3

You can also have your generator reset your iterable like this:

let iterable = generator();

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

for (let x of iterable) {
    console.log(x);
}

//Now the generator has reset the iterable and the iterable is ready to go again.

for (let x of iterable) {
    console.log(x);
}

I do not personally know the pros and cons of doing this. Just that it works as you would expect by reassigning the iterable each time the generator finishes.

EDIT: With more knowledge of how this work I would recommend just using the generator like Azder Showed:

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

for (let x of generator()) {
    console.log(x);
}

for (let x of generator()) {
    console.log(x);
}

The version I recommended will prevent you from being able to run through the iterations if it ever fails... For example if you were waiting on one url to call another. If the first url fails you would have to refresh your app to before it would be able to try that first yield again.

Sd answered 14/9, 2016 at 22:24 Comment(0)
B
1

I think this is not a concern of a generator, but of an iterator – which actually 'does the labour'. To reset an iteration you just need to create a new iterator. I would probably use a dumb higher order function like this:

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

const iterateFromStart = (func) => {
    // every invocation creates a brand new iterator   
    const iterator = func();
    for (let val of iterator) {
        console.log(val)
  }
}

iterateFromStart(foo); // 1 2 3
iterateFromStart(foo); // 1 2 3
Boomkin answered 18/10, 2018 at 15:7 Comment(0)
N
1

Per everyone else on this thread, the short answer is a hard "no". You cannot reset an iterator back to it's initial state, ESPECIALLY one produced by a generator. Depending on what generator you're dealing with, calling the generator again to give you a new iterator might even produce a completely different set of results.

The long answer is... yes, but no. You either need to iterate once to cache the results into an array before passing the results around (thus losing the performance gains from the generator), or you need to wrap it in a class instance that will handle internal state and caching of results from the iterator.

Here's an example implementation where I created a list-style class that is LIKE an array, which allows me to pass the iterator around without calling it again by caching the results and allowing multiple calls to be made LIKE an array, while still returning the same state and position of each result.

https://github.com/ahuggins-nhs/js-edi/blob/element-selectors/packages/dom/query/QueryEngineList.ts

Naturism answered 10/4, 2021 at 21:26 Comment(0)
D
1

If you want to yield values of an array and go back to the first item(after the last one has been yielded), you can do something like this:

function * arrayIterator () {
    let i = 0
    while ( i < array.length) {
        yield array[i]
        if (i === array.length - 1) i = -1 //this will reset the loop to 0 with i++
        i++
    }
}



If you want to reset a generator you can initialize it again.

let gen = arrayIterator

Let's say you wanted to return a specific value. Afterward, you will need to reset the generator like this:

const newValue = gen.return("I was returned").value

gen = arrayIterator() // now you are back at the start, when you call gen.next().value


UPDATE: I found an even simpler solution:

function * arrayIterator(){
    yield* array
}
let gen = arrayIterator()

yield* returns the next array item as if it were it's own generator.

Then you can reset the iteration like this:

let {value, done} = gen.next()
if (done === true) {
    gen = arrayIterator()
    value = gen.next().value // value at position 0
}
Dialectologist answered 26/7, 2021 at 8:23 Comment(1)
Note, that the fen function can also be replaced with: gen = array[Symbol.iterator](), making it even simplerDialectologist
K
0

No there is no going back to same state.

To make it clear, you must understand the working of generator functions.

When generator function gets called first time, it returns iterator(as its entire body). Initial status of this return iterator is get stored in its variables. Two very important variables are GeneratorStatus, GeneratorLocation.

There are are other variables such as GeneratorFunction, GeneratorReceiver, Scopes. Which can ignored to understand this answer.

So initial status will be, GeneratorStatus : suspended. GeneratorLocation: 1;

Now to use iterator you should call it using .next(); Hence forth iterator will resume its execution from location pointed by 'GeneratorLocation'

Now generator will update its value of GeneratorLocation to line no where it first yields the result and GeneratorLocation will be same until it returns last yield.

Now for Each successive calls of .next, Generator will resume its execution from value of GeneratorLocation and not from the beginning.

Hence, Unless you repeat the code in generator function, resetting to initial state is not possible. best solution is just recreate new iterator with params.

Killie answered 29/8, 2020 at 2:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.