Obviously default iterators don't work this way... That is to say: [...map] will have the same results no matter how many times it is run.
It would be possible to make an implementation where the iterable was an iterator factory instead of a singleton iterator, but I guess that would be a little more work.
class MapValuesIterable {
constructor(map) {
this.map = map;
}
[Symbol.iterator]() {
return new MapValuesIterator(this.map);
}
}
get values() { return new MapValuesIterable(this) }
My point is that if you work with any code where iterators are stored and may be evaluated repeatedly, there's just an unnecessary gotcha now where the user of the api might need to know a) that they need to write their own iterator factory and b) how to do so.
More to the point, var i = map[Symbol.iterator](); [...i, ...i] will also only produce the items once, because iterators simply aren't reusable, fundamentally.
Yeah I know that. But the point is that you almost never write out map[Symbol.iterator]() in application code. That part is always handled by array spreading or a for loop or whatever construct you're using to work with iterables, so for all intents and purposes the iterable expression is just map, and map is an iterator factory not a single iterator.
The way I break it down when I explain it to people is as stateful and stateless iterables. Stateless iterables are iterator factories under the hood, where stateful iterables are just single iterators under the hood.
Generators obviously are stateful iterables, as they must be.
Iterables over data are generally stateless, since it is perfectly reasonable to read data through the facade of an iterable as many times as you like.
Finally stateless iterables are generally implemented as a factory of stateful iterables, which I think is perfectly reasonable. You don't want turtles all the way down.
The thing is: stateless and stateful iterables are not distinguishable from each other by consumers. Both are defined using Symbol.iterator. Only convention protects users, which is why it's a particularly tough pill to swallow when convention is broken. Not even type systems can save you from those errors!
Arguably the latter is more concise, but that somewhat depends on which use case is more common.
It's also possible to enable both ways simultaneously, i.e. make .values a property whose getter returns a generator function with a [Symbol.iterator] method — not that I think it's a good idea, as it would probably cause tons of confusion.
@lightmare I just don't see why anyone would be writing map.values[Symbol.iterator](); Just like you don't write arr[Symbol.iterator](), yet you can still use arrays with any API designed for iterables (stateful or stateless). Thus in one paradigm you just use map.values and everything works because any code that only needs a single iterator will just use the factory to make a single iterator.
I agree that trying to overload a function as an iterable is not a good idea.
Good point. Any API designed for stateless iterables won't work when given a generator like myFilterAndTransform(array). Seems perfectly consistent to me that it also won't work when given array.keys() or map.values().
It is consistent -- it seems to have been an explicit choice, that's why I'm asking here. I just find it to be inconsistent with the fact that the whole iterable API was designed to force you to work through a factory layer, presumably to facilitate multi-pass flows.
Iterator objects are conveniently iterable, but they're only iterable in the sense that their [Symbol.iterator]() simply returns the iterator itself. When the iterator is exhausted, so should it be as an iterable since that is the nature of iterator objects.
Objects like Map or Array objects are not iterators. They're collections that are iterable, but not themselves iterators. Unlike iterators it is not their inherent nature to exhaust themselves so they can be allowed to support multiple iterations. These iterations individually create exhaustible iterators but thats ok because they are meant be one-time use and for any subsequent iteration a new iterator can be created. And when we need additional iterations, we should be going to the source, the Map or Array etc., not be looking to an iterator from an earlier iteration since its the source collection we'd want to iterate over.
Assuming iterators did support multi-pass iterations, you'd have conflicting results depending on how you approached getting their data. Going through the iterator protocol API with next() you'd be picking up where you left off, but as an iterable, you'd be restarting the iteration. Mixing the two wouldn't be possible
const values = map.values()
values.next() // skip first
for (let value of values) {
// restarting iterator, not skipping first after all?
}
Iterators today would start the loop from the second value since the iterator returned from the iterable is the iterator itself and not a new one. I would consider this is the preferred behavior.
That's an interesting snippet. But I really don't mind if something that unusual has to be written as
const values = map.values[Symbol.iterator]()
values.next() // skip first
for (let value of values) {
// restarting iterator, not skipping first after all?
}
Iterator objects are conveniently iterable, but they're only iterable in the sense that their [Symbol.iterator]() simply returns the iterator itself.
I agree! And what is an iterator? It's something that fulfills the iterator protocol. But how do you know it fulfills the iterator protocol? Because it was returned from a call to [Symbol.iterator]()!
Imagine you're writing your code up as a reusable method:
The correct code for the method must call [Symbol.iterator]() because otherwise calling .next() is not safe.
And finally note this: that method works equally well with an "iterator" or an "iterable", because javascript doesn't really distinguish. There are only stateful and stateless iterables:
But what is this returning? If map.values() returns a MapIterator, would map.values[Symbol.iterator]() return a MapIteratorIterator? I imagine it couldn't be a MapIterator because they're multi-pass and we'd expect this one to be single. In fact given any iterator, how would you know its single or multi? Just being an iterator doesn't tell you (like it does now). I think the consistent behavior would be turtles all the way down.
Nope it's implemented as { [Symbol.iterator]() { return this; }}. An "iterator" is just an iterable that returns itself, which makes it fully stateful as there is now only once instance. All iterators are (or should be) iterables.
My point is just that iterators that aren't iterables shouldn't exist. They have no purpose, as they break even code that expects iterators, as your example confirms.
I'm coming at this from the library perspective. I maintain iter-tools, which has the following philosophy: never assume that the source can be iterated more than once, but always try to make a new source iterator when making a new result iterator. This way if a source passes through a series of methods like map and filter, the result will be multi-pass if the source is. But that's the best guarantee we can make. If the source is single-pass so too will your result be.
It is purely a convenience that all built-in iterators are themselves iterable.
A convenience it may have been intended as, but now we're stuck not being able to tell the difference between iterables (a.k.a. iterator factories, stateless iterables, multi-pass iterables, etc) and iterators. Most often this manifests as inability to determine whether whether an error condition has occurred when trying to get a new iterator. If the original iterable was just an iterator with the "convenience" [Symbol.iterator]() { return this; } attached, then you have actually failed to make a new iterator. If the developer wants a new iterator over the data and it isn't possible to make one, it's pretty difficult to work around when they can't even tell that it has happened.
Take a promise returned from a fetch(). If you want a new fetch request, you wouldn't go to the promise returned by the last fetch call, you'd call fetch() again. Similarly, if you want a new iterator, you wouldn't want to look to a previous iteration's iterator, you'd go through the data intended to be iterated.
Ideally you wouldn't be in a position to have to worry about whether or not something is a source of data or an iterator for that data. If you had to make that distinction, I think the best you could do was check for a next() though that's no guarantee.
Right. But as a library author I can't tell the two apart. I don't get to see the calling code, I just get passed some objects and all those objects look the same, so I have no chance to warn the user that they used an iterator as an iterable which was probably a coding error.