5

I've been implementing a useful subclass of the ES6 Set object. For many of my new methods, I want to accept an argument that can be either another Set or an Array, or really anything that I can iterate. I've been calling that an "iterable" in my interface and just use .forEach() on it (which works fine for a Set or an Array. Example code:

// remove items in this set that are in the otherIterable
// returns a count of number of items removed
remove(otherIterable) {
    let cnt = 0;
    otherIterable.forEach(item => {
        if (this.delete(item)) {
            ++cnt;
        }
    });
    return cnt;
}

Or

// add all items from some other iterable to this set
addTo(iterable) {
    iterable.forEach(item => {
        this.add(item);
    });
}

But, I suspect I may be not really supporting any iterable in the way that ES6 defines it so I'm interested in what the real definition of a Javascript iterable is using the term as the ES6 specification does?

How do you test for it in ES6 Javascript?

How should you iterate a generic iterable?

I've found phrases like this in the ES6 specification:

If the parameter iterable is present, it is expected to be an object that implements an @@iterator method that returns an iterator object that produces a two element array-like object whose first element is a value that will be used as a WeakMap key and whose second element is the value to associate with that key.

But, that refers to an @@iterator method which I don't seem to be able to access via that property name.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Isn't that "@@iterator" thing supposed to be a "system" Symbol instance or something? *edit* I think it's `Symbol.iterator` – Pointy Jan 10 '17 at 00:09
  • @Pointy - Maybe, but I'm not really sure how all this Symbol stuff works and how I'm supposed to use it or what that means for how I'm supposed to iterate the object. – jfriend00 Jan 10 '17 at 00:10
  • 1
    Well Symbol instances work as object property names. They're never enumerable. So like you can do `foo[Symbol.iterator] = function() ...` (or put it on the prototype) – Pointy Jan 10 '17 at 00:11
  • I guess that should be an answer if I understand your question. – Pointy Jan 10 '17 at 00:12
  • I'm not sure whether this should be closed as a duplicate of [ES 6: Difference between Symbol.iterator and @@iterator](http://stackoverflow.com/q/29670320/1048572) or [What does @@ (“at at”) mean in ES6 JavaScript?](http://stackoverflow.com/q/29492333/1048572), or whether we should edit the question and answer that an iterable is an object with an iterator method. – Bergi Jan 10 '17 at 00:13
  • @Bergi - I don't think that other question explains how you're supposed to iterate something that you discover is an iterable. It does help explain the `Symbol.iterator` thing. – jfriend00 Jan 10 '17 at 00:14
  • @jfriend00 Yes, my problem with the question is that it's asking both "*how to test for/use iterables*" (while providing the answer to that in the quote) and "*what does @@iterator mean and how to use it as a property*". Can you edit it into one of them? (Or split it off into a second post) – Bergi Jan 10 '17 at 00:17
  • @Bergi - Well I'm trying to solve a problem that involves both (thus why I showed the actual problem, not just asked a theoretical question). Can the question really not cover both aspects of the problem? – jfriend00 Jan 10 '17 at 00:18
  • I just thought we'd only put the actual problem in the question and leave the discussion of what `@@iterator` means to the answers if they want to use the quote you found - or don't discuss it but put a link. – Bergi Jan 10 '17 at 00:27

2 Answers2

9

What is the real definition of a Javascript iterable using the term as the ES6 specification does?

§25.1.1.1 defines "The Iterable Interface".

They're objects with a Symbol.iterator-keyed method that returns a valid Iterator (which in turn is an object expected to behave as it should according to §25.1.1.2).

How do you test for it in ES6 Javascript?

We cannot test what the @@iterator method returns without calling it, and we cannot test whether the result conforms to the Iterator interface without trying to run it. The best bet would be to do

function looksIterable(o) {
    return typeof o[Symbol.iterator] == "function";
}

however I wouldn't usually test for this but simply let it fail with an exception when it's not iterable.

How should you iterate a generic iterable?

Don't use forEach. (In fact, never use forEach anywhere in ES6).

The proper way to iterate is a for (… of …) loop. It does all the checking for iterability (using the abstract GetIterator operation and running (and even closing) the iterator, and throws appropriate TypeErrors when used on non-iterable values.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 1
    I went to convert some `.forEach()` code to `for/of`. Most was simple to convert, but the functions that use the `index` argument that `.forEach()` provides require extra code to keep track of the index with `for/of`. I'm not sure I'm ready yet to say that one should never use `.forEach()`. Certainly, there appears to be no reason to use it with ES6 when you aren't using the `index` argument from `.forEach()`. One of the original reasons I was using `.forEach()` is that it can work with polyfill collections in ES5, whereas `for/of` can't be polyfilled (it has to be transpiled). – jfriend00 Jan 10 '17 at 10:43
  • Most of the time where you need indices, [`arr.entries()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/entries) is your friend. If you need an index on arbitrary iterables, you can abstract out the extra code into `function* withIndex(it) { let i=0; for (let v of it) yield [i++, v]; }` and use it like e.g. `for (let [i, el] of withIndex(set)) …`. – Bergi Jan 10 '17 at 15:04
  • Yeah, it can certainly be done that way (or a multitude of other ways too). They are not simpler than `.forEach()` though when you want both value and index for each iteration. Why is it you so strongly recommend against `.forEach()` when it's the simplest match for what you're doing? Are you just trying to avoid the multiple function calls it uses? – jfriend00 Jan 10 '17 at 19:46
  • 1
    Yes, overhead of callback invocation is one of the reasons (admittedly I have no idea how it compares to the overhead of generators, and don't know which is likelier to get optimiser care). The other two: iterables are more generic (better) than "forEachables", and that I absolutely hate side-effectful programming with `forEach` (especially when chosen where `map` or `reduce` are more appropriate) so I prefer using loops for imperative style as a useful distinction from purely functional style. Maybe I should write a *`forEach` considered harmful* essay :-D – Bergi Jan 10 '17 at 20:12
  • 2
    Reading between the lines here, it's feeling to me like you hate when people use `.forEach()` for the wrong reasons (instead of `.map()` and `.reduce()` for example) and now that we have `for/of` (if you program in an ES6 environment or transpile), there are even fewer reasons to use `.forEach()`, but it doesn't actually sound like you have a legit case for getting rid of it entirely when it's the legitimately simplest match to what you're doing. – jfriend00 Jan 10 '17 at 20:39
  • 1
    Yeah, I guess that sums it up nicely, I just got into the habit of saying "*don't use `forEach` at all*" which works for 99% percent of ES6 use cases, instead of detailing where it is legit (probably only things like `readLines().map(…).filter(…).forEach(writeLine)` or when my keystrokes are especially precious). – Bergi Jan 10 '17 at 20:51
  • I do not see any reasons not to use forEach - MUCH more readable when appropriate and much more appropriate in many cases than any other loop – mplungjan Jan 13 '21 at 12:58
  • @mplungjan Except that `forEach` is not a loop - and that makes it inappropriate for people who don't understand the difference. – Bergi Jan 13 '21 at 13:17
  • @Bergi I do not see an explanation other than "don't use it" in your explanation above. How is forEach not a loop? I understand for is faster than forEach, but who cares for 100 records – mplungjan Jan 13 '21 at 13:19
  • @mplungjan it's a method with a callback function, not a control structure, so control flow keywords don't work like [many](https://stackoverflow.com/q/2641347/1048572) [people](https://stackoverflow.com/q/34104231/1048572) [expect](https://stackoverflow.com/q/37576685/1048572). (I don't care about performance differences, which are negligible) – Bergi Jan 13 '21 at 13:29
  • @Bergi Ah, yeah, break/continue/return etc. I do not need them when I use forEach. If I do I will use a for :) – mplungjan Jan 13 '21 at 13:33
1

There's a special property on the Symbol constructor, Symbol.iterator, whose value is that, uhh, I guess you'd say "conceptual" property name "@@iterator". So you can create an iterator method for an object like this:

object[Symbol.iterator] = function* () {
  // do something that makes sense
};

You can also test to see if some object is an iterator by doing

if (Symbol.iterator in object)

(and maybe also checking to see if it's a function). Now those iterator functions (the value of the Symbol.iterator property) are generator functions (not the * that I edited into the example). Thus you start them and get values by first calling the function and saving the returned object, and then calling .next(). That'll get you an object with value and done properties.

You can alternatively let for ... of or the "spread" ... operator worry about the iterator function.

Pointy
  • 405,095
  • 59
  • 585
  • 614
  • So, that answers how you test if something is an official iterable. But, then how are you supposed to iterate it in the most generic way? Are you just supposed to use `for/of`? – jfriend00 Jan 10 '17 at 00:15
  • @jfriend00 sure, `for ... of` or you can turn it into an array with `Array.from()` or destructuring assignment. – Pointy Jan 10 '17 at 00:20
  • Also note that the iterator function is *I think* supposed to be a `function*` function, but I'm not 100% sure I really understand that. (fixed answer) – Pointy Jan 10 '17 at 00:21
  • 2
    `function*` is one way to make a function that returns an iterator object, but it is fine to have a normal function that returns a conforming iterator object. – loganfsmyth Jan 10 '17 at 01:21
  • @loganfsmyth OK yes of course that makes perfect sense. – Pointy Jan 10 '17 at 04:49