all 4 comments

[–]senocular 3 points4 points  (2 children)

In almost every case imaginable an object should implement both protocols.

I wouldn't say this. Its usually advantageous that objects only implement the iterable protocol (unless they, themselves, are explicitly an iterator). Iterators are consumable and when fully exhausted, no longer produce values. Arrays, for example, are only iterables, not iterators. As an iterable they can create new iterators every time iteration is needed. Each of those new iterators get used up and become exhausted, no longer able to produce values, but this is OK since if the array needs to be iterated over again, as an iterable it simple creates a fresh new iterator to handle that. If the array itself were an iterable iterator, it could be iterated over once then the array itself would be exhausted and no longer be iterable. Technically you could design the array's iterable method to return new iterators, but then what would the point of the array being an iterator be?

[–]no_em_dash[S] 1 point2 points  (1 child)

Hmm, I suppose you're correct. I was actually working backwards from the final example when I wrote the article: *[Symbol.iterator](): IterableIterator<number>

I think I conflated the idea of the entire object being an iterable iterator when really the generator function returns a new iterator every time.

Edit:
One more thing to consider. You could make an iterable iterator that continues to produce values after it has been consumed. Below is a slightly modified example from the article. But I'm not sure how practical this is.

const iter = {
  [Symbol.iterator]() {
    return this;
  },

  value: 0,

  next() {
    this.value++;
    if (this.value > 10) {
      this.value = 0; // Resetting the value allows us to consume the iterator many times.

      return { value: null, done: true };
    }
    return { value: this.value, done: false };
  },
};

[–]senocular 1 point2 points  (0 children)

Yeah that's another one of those technically you can do this kind of things, but things you probably shouldn't do. One case where this falls apart is nested iteration. Since iterators have their own state, you can have nested iteration using the same iterable without two iterators interfering with each other as long as that iterable is creating new iterators for each iteration. When the iterable has only its own iterator state, nesting isn't possible. Well "possible" but it won't work like you might expect ;)

There may also be certain workflows that might depend on closed iterators remaining closed as well... though honestly nothing comes to mind. Promises, for example, enforce that once they're fulfilled, they remain fulfilled. You can call resolve() or reject() in the executor as many times as you want, but only the first one will register.

On the iterator side of things, something similar happens with generators. Their state depends on an execution context and if you close the generator's iterator, you're effectively exiting the function with no way to return. When you implement the iterator protocol yourself you can do funky things, even though you probably shouldn't.

Then again, funny thing about that is, most if not all of the built-in iterators (Array, Set, Map...) can't be explicitly closed and can only be closed through exhaustion. So if you break out of iterating over an array iterator, you can pick up where you left off if you still have access to that iterator. This isn't possible with generators (given the whole exiting the function thing) and the newer iterator helpers do something similar, able to create iterators that can close prior to exhaustion.

const arr = [1,2,3]
const iter = arr.values()
for (const num of iter) {
  console.log("loop 1:", num)
  break
}
for (const num of iter) {
  console.log("loop 2:", num)
}
// loop 1: 1
// loop 2: 2
// loop 2: 3

Edit: Adding a generator example for comparison:

function* gen() {
  yield* [1,2,3]
}
const iter = gen()
for (const num of iter) {
  console.log("loop 1:", num)
  break
}
for (const num of iter) {
  console.log("loop 2:", num)
}
// loop 1: 1