Reputation: 1633
I'm learning about iterators/generators, and am wondering if there is a way to make the iterator reset itself?
For example, when after you call the last .next()
is there a way to make it restart?
const arr = [ {name: "Bob"}, {name: "Peter"}, {name: "Sally"} ];
const names = nameGenerator(arr);
function nextName() {
const current = names.next().value;
const output = document.querySelector("output");
if (current) {
output.innerText = current.name;
}
else {
// code to reset it!
output.innerText = 'Nothing left';
}
}
function nameIterator(names) {
let nextIndex = 0;
return {
next: function () {
return nextIndex < names.length ?
{ value: names[nextIndex++], done: false } :
{ done: true };
}
}
}
function* nameGenerator() {
let index = 0;
while (index < arr.length) {
yield arr[index++];
}
}
document.querySelector("button").addEventListener("click", nextName)
<button>Iterate me!</button>
<output></output>
In this snippet, when you click the button, it calls my iterator which will display the names in my names array one by one each time you click. How can I make it reset on the last click so it goes back to the first name?
So the display would be like:
Bob Peter Sally Bob // get Bob again
I have also written the iterator using a generator, is there anyway for that to also be able to reset back to the first one after yielding through the array?
Upvotes: 4
Views: 3672
Reputation: 351039
You can send something to your generator while consuming it, so it can act differently depending on that value, and perform an action like resetting.
In your example you want to cycle back at the end, but if in general you have another condition whereby you want to reset the iterator somewhere half-way, then using the argument of the iterator next
method could be useful.
To demonstrate this, I have extended your snippet with a second button, with which the user can choose to reset the iterator at any time. It will also cycle if the user just keeps clicking the Next button. Note how yield
returns the value that you pass to the next
call:
const arr = [ {name: "Alice"}, {name: "Bob"}, {name: "Carol"}, {name: "Danny"} ];
const names = nameGenerator(arr);
function nextName(reset=false) {
// Pass a boolean to the iterator's next method:
const current = names.next(reset).value;
const output = document.querySelector("output")
output.innerText = current.name;
}
function* nameGenerator(names) {
// Here we make an infinite sequence by wrapping the index around
for (let i = 0; true; i = (i + 1) % names.length) {
// Get the value that the consumer passed to `next`:
const reset = yield names[i];
// Did they want to reset?
if (reset) i = -1; // The loop's increment will make it 0
}
}
// Get the two buttons and pass a boolean to the nextName function
const [btnNext, btnFirst] = document.querySelectorAll("button");
btnNext.addEventListener("click", () => nextName(false));
btnFirst.addEventListener("click", () => nextName(true));
<button>Get Next</button>
<button>Get First</button>
<output></output>
Upvotes: 0
Reputation: 431
I came to the thread to look for the answer myself, and here's what I came up with:
class CyclicIter {
constructor(arr) {
this.arr = arr
this._at = -1
}
[Symbol.iterator]() {
return {
next: () => {
if (this._at < this.arr.length) {
return {
value: this.arr[this._at++],
done: false
}
} else {
this.reset()
return {
done: true
}
}
}
};
};
reset() {
this._at = -1
}
};
const myArray = [1, 2, 3, 4]
const myArrayIterable = new CyclicIter(myArray)
console.log(...myArrayIterable); // can be called multiple times
this example may be trivial since I am super new to JS. Any suggestions are appreciated.
Upvotes: 0
Reputation: 386766
You could change the index and take the remainder value, if the index is greater than the array index.
function nameIterator(names) {
let nextIndex = 0;
return {
next: function() {
nextIndex++;
nextIndex %= name.length;
return { value: names[nextIndex], done: false };
}
}
}
Upvotes: 1
Reputation: 175048
The general answer is that you can't reset an iterable in JS. This is, as far as I know, by-design, although I'm not privy to the decision-making process that was used to reach that conclusion.
In your specific case however, you don't want to necessarily reset the iterator, you want to cycle through the array. In that case, you can use the modulo operator as follows:
function nameIterator(names) {
let nextIndex = 0;
return {
next: function() {
nextIndex = (nextIndex + 1) % names.length;
return { value: names[nextIndex], done: false };
}
}
}
Upvotes: 5
Reputation: 17636
To solve this, when the iterator is done reset it like so:
// code to reset it!
output.innerText = 'Nothing left'
names = nameGenerator(arr);
Unless someone proves me wrong, there is currently no method to reset an iterator that I'm aware of.
Working snippet example:
const arr = [ {name: "Bob"}, {name: "Peter"}, {name: "Sally"} ];
let names = nameGenerator(arr);
function nextName() {
const current = names.next().value
const output = document.getElementById("output")
if (current) {
output.innerText = current.name
}
else {
// code to reset it!
output.innerText = 'Nothing left'
names = nameGenerator(arr);
}
}
function nameIterator(names) {
let nextIndex = 0;
return {
next: function() {
return nextIndex < names.length ?
{ value: names[nextIndex++], done: false } :
{ done: true }
}
}
}
function* nameGenerator() {
let index = 0;
while(index < arr.length) {
yield(arr[index++])
}
}
document.querySelector("button").addEventListener("click", nextName)
<button id="button">click me</button>
<p id="output"></p>
Upvotes: 1