ECMAScript proposal: iterator helpers

[2022-12-15] dev, javascript, es proposal
(Ad, please don’t block)

Update 2022-12-15: New section “How will this proposal affect future JavaScript APIs?”


In this blog post, we look at the ECMAScript proposal “Iterator helpers” by Gus Caplan, Michael Ficarra, Adam Vandolder, Jason Orendorff, Kevin Gibbons, and Yulia Startsev. It introduces utility methods for working with iterable data: .map(), .filter(), .take(), etc.

The style of the proposed API clashes with the style of the current iteration API. We’ll explore how we can fix that.

Synchronous versus asynchronous iteration  

JavaScript supports two kinds of iteration modes:

We’ll first explore synchronous iteration in depth and then briefly look at asynchronous iteration – the asynchronous part of the proposal is very similar to the synchronous part.

Synchronous iteration  

Iteration: a protocol for sequential data consumers  

In JavaScript, there are several constructs that consume data sequentially – one value at a time – for example, the for-of loop and spreading into Arrays.

A protocol consists of interfaces and rules for using them.

The iteration protocol is used by JavaScript’s sequential data consumers to access their input. Any data structure that implements this protocol can therefore be consumed by them.

The interfaces of the iteration protocol  

The following roles are involved in the synchronous iteration protocol:

  • Values that support the iteration protocol are called iterables. They return iterators via a method [Symbol.iterator]().

  • We get the iterated values by repeatedly invoking method .next() of the iterator returned by the iterable.

These are the TypeScript types for these roles:

interface Iterable<T> {
  [Symbol.iterator]() : Iterator<T>;
}

interface Iterator<T> {
  next() : IteratorResult<T>;
}

interface IteratorResult<T> {
  done: boolean;
  value?: T;
}

The Iterator method .next() returns:

  • An object {done: false, value: x} for each iterated value x.
  • The object {done: true} after the last iterated value.

In other words: We call .next() until it returns an object whose property .done is true.

As an example, let’s use the iteration protocol to access the elements of a Set:

const iterable = new Set(['hello', 'world']);
const iterator = iterable[Symbol.iterator]();

assert.deepEqual(
  iterator.next(),
  { value: 'hello', done: false }
);
assert.deepEqual(
  iterator.next(),
  { value: 'world', done: false }
);
assert.deepEqual(
  iterator.next(),
  { value: undefined, done: true }
);

Since Sets are iterable, we can use them with iteration-based data consumers such as spreading into Arrays (line A) and for-of loops (line B):

const iterable = new Set(['hello', 'world']);

assert.deepEqual(
  [...iterable], // (A)
  ['hello', 'world']
);

for (const x of iterable) {
  console.log(x);
}
// Output:
// hello
// world

Note that we never saw iterators – those are only used internally by the consumers.

The JavaScript standard library has more iteration-based data producers and consumers. We’ll look at these next.

Iteration-based data producers  

These data structures are iterable:

  • strings
  • Arrays
  • Sets
  • Maps

The following data structures have the methods .keys(), .values(), and .entries() that return iterables that are not Arrays:

  • Arrays
  • Sets
  • Maps

Synchronous generator functions and methods expose their yielded values via iterable objects that they return:

/** Synchronous generator function */
function* createSyncIterable() {
  yield 'a';
  yield 'b';
  yield 'c';
}

We’ll use the result of createSyncIterable() to demonstrate iteration-based data consumers in the next subsection.

Iteration-based data consumers  

All of the following constructs access their input via the iteration protocol.

The for-of loop:

for (const x of createSyncIterable()) {
  console.log(x);
}
// Output:
// a
// b
// c

Spreading:

// Spreading into Arrays
assert.deepEqual(
  ['>', ...createSyncIterable(), '<'],
  ['>', 'a', 'b', 'c', '<']
);

// Spreading into function arguments
const arr = [];
arr.push('>', ...createSyncIterable(), '<');
assert.deepEqual(
  arr,
  ['>', 'a', 'b', 'c', '<']
);

Array.from():

assert.deepEqual(
  Array.from(createSyncIterable()),
  ['a', 'b', 'c']
);
assert.deepEqual(
  Array.from(createSyncIterable(), s => s + s),
  ['aa', 'bb', 'cc']
);

Array-destructuring:

const [elem0, elem1] = createSyncIterable();
assert.equal(elem0, 'a');
assert.equal(elem1, 'b');

Processing iterables via generators  

Generators produce iterables, but they can also consume them. That makes them a versatile tool for transforming iterables:

function* map(iterable, callback) {
  for (const x of iterable) {
    yield callback(x);
  }
}
assert.deepEqual(
  Array.from(
    map([1, 2, 3, 4], x => x ** 2)
  ),
  [1, 4, 9, 16]
);

function* filter(iterable, callback) {
  for (const x of iterable) {
    if (callback(x)) {
      yield x;
    }
  }
}
assert.deepEqual(
  Array.from(
    filter([1, 2, 3, 4], x => (x%2) === 0
  )),
  [2, 4]
);

The inheritance of the current iteration API  

All of the iterators created by JavaScript’s standard library have a common prototype which the ECMAScript specification calls %IteratorPrototype%.

Array iterators  

We create an Array iterator like this:

const arrayIterator = [][Symbol.iterator]();

This object has a prototype with two properties. Let’s call it ArrayIteratorPrototype:

const ArrayIteratorPrototype = Object.getPrototypeOf(arrayIterator);
assert.deepEqual(
  Reflect.ownKeys(ArrayIteratorPrototype),
  [ 'next', Symbol.toStringTag ]
);
assert.equal(
  ArrayIteratorPrototype[Symbol.toStringTag],
  'Array Iterator'
);

The prototype of ArrayIteratorPrototype is %IteratorPrototype%. This object has a method whose key is Symbol.iterator. Therefore, all built-in iterators are iterable.

const IteratorPrototype = Object.getPrototypeOf(ArrayIteratorPrototype);
assert.deepEqual(
  Reflect.ownKeys(IteratorPrototype),
  [ Symbol.iterator ]
);

The prototype of IteratorPrototype is Object.prototype.

assert.equal(
  Object.getPrototypeOf(IteratorPrototype) === Object.prototype,
  true
);

This is a diagram for this chain of prototypes:

A chain of prototypes: first , then , then  and finally .

Generator objects  

Roughly, a generator object is an iterator for the values yielded by a generator function genFunc(). We create it by calling genFunc():

function* genFunc() {}
const genObj = genFunc();

The prototype of genObj is genFunc.prototype:

assert.equal(
  Object.getPrototypeOf(genObj) === genFunc.prototype,
  true
);
assert.deepEqual(
  Reflect.ownKeys(genFunc.prototype),
  []
);

The prototype of genFunc.prototype is an object that is shared with all generator objects. In addition to the iterator method .next(), it has generator-specific methods such as .return() and .throw(). The ECMAScript specification calls it %GeneratorFunction.prototype.prototype%:

const GeneratorFunction_prototype_prototype =
  Object.getPrototypeOf(genFunc.prototype);

assert.deepEqual(
  Reflect.ownKeys(GeneratorFunction_prototype_prototype),
  [
    'constructor',
    'next',
    'return',
    'throw',
    Symbol.toStringTag,
  ]
);
assert.equal(
  GeneratorFunction_prototype_prototype[Symbol.toStringTag],
  'Generator'
);

The prototype of %GeneratorFunction.prototype.prototype% is %IteratorPrototype%:

const p = Object.getPrototypeOf;
const IteratorPrototype = p(p([][Symbol.iterator]()));
assert.equal(
  Object.getPrototypeOf(GeneratorFunction_prototype_prototype),
  IteratorPrototype
);

The rightmost column of this diagram shows the following prototype chain: , , , , .

Why are the built-in iterators iterable?  

As we have seen, generator objects are, at their cores, iterators (they have a method .next()), not iterables. However, we’d also like to use generators to implement iterables. That’s why generator objects have a method [Symbol.iterator]() that returns this. They inherit this method from %IteratorPrototype%.

The following code demonstrates that each generator object returns itself when it is asked for an iterator:

function* gen() {}

const genObj = gen();
assert.equal(
  genObj[Symbol.iterator](),
  genObj
);

Iteration quirk: two kinds of iterables  

Alas, iterable iterators mean that there are two kinds of iterables:

  1. Iterable iterators are one-time iterables: They always return the same iterator when [Symbol.iterator]() is called (iteration continues).

  2. Arrays, Sets, etc. are many-times iterables: They always return fresh iterators (iteration restarts).

const iterOnce = ['a', 'b', 'c'].values();
assert.deepEqual(
  [...iterOnce, ...iterOnce, ...iterOnce],
  ['a', 'b', 'c']
);

const iterMany = ['a', 'b', 'c'];
assert.deepEqual(
  [...iterMany, ...iterMany, ...iterMany],
  ['a','b','c', 'a','b','c', 'a','b','c']
);

The new API: synchronous iteration  

We have already seen that %IteratorPrototype% is the prototype of all built-in iterators. The proposal introduces a class Iterator:

  • Iterator.from() is a utility method that we’ll explore soon.
  • Iterator.prototype refers to %IteratorPrototype%.
  • %IteratorPrototype%.constructor refers to Iterator.
  • Iterator.prototype contains various methods that are inherited by iterators – for example:
    • Iterator.prototype.map(mapFn) returns a mapped version of this
    • Iterator.prototype.take(limit) returns an iterator for the first limit values of this.
    • Iterator.prototype.toArray() returns an Array with the values of this.

Iterator.from(): creating API iterators  

The static method Iterator.from(x) returns an instanceof Iterator:

  • If x is a synchronous API iterable, it returns x[Symbol.iterator]().
  • If x is a synchronous API iterator, it returns x unchanged.
  • If x is a synchronous legacy iterator (that doesn’t support the new API), it wraps it so that it supports the new API and returns the result.
  • If x is a synchronous legacy iterable, it wraps the result of x[Symbol.iterator]() and returns it.

In the following example, we use Iterator.from() to convert a legacy iterator to an API iterator:

// Not an instance of `Iterator`
const legacyIterator = {
  next() {
    return { done: false, value: '#' };
  }
};
assert.equal(
  Iterator.from(legacyIterator) instanceof Iterator,
  true
);
assert.deepEqual(
  Iterator.from(legacyIterator).take(3).toArray(),
  ['#', '#', '#']
);

An overview of the new Iterator.prototype methods  

The following subsections give an overview of the new Iterator.prototype methods. They will use this function to create a synchronous iterable:

function* createSyncIterator() {
  yield 'a'; yield 'b'; yield 'c'; yield 'd';
}

Some of the iterator methods keep a counter for the iterated values and pass it on to their callbacks:

  • .every()
  • .filter()
  • .find()
  • .flatMap()
  • .forEach()
  • .map()
  • .reduce()
  • .some()

Iterator methods that return iterators  

iterator.take(limit)  

This method returns an iterator with the first limit values of iterator.

Type signature:

Iterator<T>.prototype.take(limit: number): Iterator<T>

Example:

assert.deepEqual(
  createSyncIterator().take(1).toArray(),
  ['a']
);

iterator.drop(limit)  

This method returns an iterator that with all values of iterator, except for the first limit ones. That is, iteration starts when the iteration counter is limit.

Type signature:

Iterator<T>.prototype.drop(limit: number): Iterator<T>

Example:

assert.deepEqual(
  createSyncIterator().drop(1).toArray(),
  ['b', 'c', 'd']
);

iterator.filter(filterFn)  

This method returns an iterator whose values are the values of iterator for which filterFn returns true.

Type signature:

Iterator<T>.prototype.filter(
  filterFn: (value: T, counter: number) => boolean
): Iterator<T>

Example:

assert.deepEqual(
  createSyncIterator().filter(x => x <= 'b').toArray(),
  ['a', 'b']
);

iterator.map(mapFn)  

This method returns an iterator whose values are the result of applying mapFn to the values of iterator.

Type signature:

Iterator<T>.prototype.map<U>(
  mapFn: (value: T, counter: number) => U
): Iterator<U>

Example:

assert.deepEqual(
  createSyncIterator().map(x => x + x).toArray(),
  ['aa', 'bb', 'cc', 'dd']
);

iterator.flatMap(mapFn)  

This method returns an iterator whose values are the values of the iterables or iterators that are the results of applying mapFn to the values of iterator.

Type signature (simplified):

Iterator<T>.prototype.flatMap<U>(
  mapFn: (value: T, counter: number) => Iterable<U> | Iterator<U>
): Iterator<U>

Example:

assert.deepEqual(
  createSyncIterator()
  .flatMap((value, counter) => new Array(counter).fill(value))
  .toArray(),
  ['b', 'c', 'c', 'd', 'd', 'd']
);

For more information on .flatMap(), see the section on the related Array method in “JavaScript for impatient programmers”.

Iterator methods that return non-iterators  

iterator.some(fn)  

This method returns true if fn returns true for at least one value of iterator. Otherwise, it returns false.

Type signature:

Iterator<T>.prototype.some(
  fn: (value: T, counter: number) => boolean
): boolean

Example:

assert.equal(
  createSyncIterator().some(x => x === 'c'),
  true
);

iterator.every(fn)  

This method returns true if fn returns true for every value of iterator. Otherwise, it returns false.

Type signature:

Iterator<T>.prototype.every(
  fn: (value: T, counter: number) => boolean
): boolean

Example:

assert.equal(
  createSyncIterator().every(x => x === 'c'),
  false
);

iterator.reduce(reducer, initialValue?)  

This method uses the function reducer to combine the values of iterator into a single value.

Type signature:

Iterator<T>.prototype.reduce<U>(
  reducer: (accumulator: U, value: T, counter: number) => U,
  initialValue?: U
): U

Example – concatenating the strings of an iterator:

assert.deepEqual(
  createSyncIterator().reduce((acc, v) => acc + v),
  'abcd'
);

Example – computing the minimum of a Set of numbers:

const set = new Set([3, -2, -5, 4]);
assert.equal(
  set.values().reduce((min, cur) => cur < min ? cur : min, Infinity),
  -5
);

For more information on .reduce(), see the section on the related Array method in “JavaScript for impatient programmers”.

iterator.find(fn)  

This method returns the first value of iterator for which fn returns true. If there is no such value, it returns undefined.

Type signature:

Iterator<T>.prototype.find(
  fn: (value: T, counter: number) => boolean
): T

Example:

assert.equal(
  createSyncIterator().find((_, counter) => counter === 1),
  'b'
);

Looping and conversion  

iterator.forEach(fn)  

This method applies fn to each value in iterator.

Type signature:

Iterator<T>.prototype.forEach(
  fn: (value: T, counter: number) => void
): void

Example:

const result = [];
createSyncIterator().forEach(x => result.unshift(x))
assert.deepEqual(
  result,
  ['d', 'c', 'b', 'a']
);

iterator.toArray()  

This method returns the values of iterator in an Array.

Type signature:

Iterator<T>.prototype.toArray(): Array<T>

Example:

assert.deepEqual(
  createSyncIterator().toArray(),
  ['a', 'b', 'c', 'd']
);

iterator.toAsync()  

This method returns an asynchronous iterator for the values of the synchronous iterator.

Type signature:

Iterator<T>.prototype.toAsync(): AsyncIterator<T>

Example:

assert.equal(
  createSyncIterator() instanceof AsyncIterator,
  false
);
assert.equal(
  createSyncIterator().toAsync() instanceof AsyncIterator,
  true
);

Using the new API with legacy iterables  

All built-in iterables automatically support the new API because their iterators already have Iterator.prototype as a prototype (and are therefore instances of Iterator).

However, that’s not the case for many iterables in libraries and user code.

Example: a manually implemented iterable  

This is an example of a manually implemented iterable:

class MyIterable {
  #values;
  #index = 0;
  constructor(...values) {
    this.#values = values;
  }
  [Symbol.iterator]() {
    return {
      // Arrow function so that we can use the outer `this`
      next: () => {
        if (this.#index >= this.#values.length) {
          return {done: true};
        }
        const value = this.#values[this.#index];
        this.#index++;
        return {done: false, value};
      },
    };
  }
}
assert.deepEqual(
  Array.from(new MyIterable('a', 'b', 'c')),
  ['a', 'b', 'c']
);

This iterable does not support the new API. We can use Iterator.from() to convert an instance of MyIterable to an API iterator:

const legacyIterable = new MyIterable('a', 'b', 'c');
assert.deepEqual(
  Iterator.from(legacyIterable).take(2).toArray(),
  ['a', 'b']
);

If we want MyIterable to support the new API, we have to make its iterators instances of Iterator:

class MyIterable {
  // ···
  [Symbol.iterator]() {
    return {
      __proto__: Iterator.prototype,
      next: () => {
        // ···
      },
    };
  }
}

This is another option:

class MyIterable {
  // ···
  [Symbol.iterator]() {
    return Iterator.from({
      next: () => {
        // ···
      },
    });
  }
}

Example: Immutable.js  

The iterables provided by the library Immutable.js don’t support the new API, either. Their iterators are currently implemented like this (source):

class Iterator {
  constructor(next) {
    this.next = next;
  }
  toString() {
    return '[Iterator]';
  }
  [Symbol.iterator]() {
    return this;
  }
  // ···
}

To support the new API, class Iterator has to be renamed and extend the API’s class Iterator:

class CustomIterator extends Iterator {
  // ···
}

We can also use Iterator.from() to convert Immutable.js iterables to API iterators.

How to unify the two clashing iteration styles  

Before the new API, JavaScript’s iteration had an iterable style:

  • The iterable is the dominant iteration role.
  • All built-in language constructs operate on iterables. Programmers using these constructs never see iterators.
  • This style is also used by Java and Python.

The new API has an iterator style:

  • The iterator is the dominant iteration role. The API never uses iterables (except to support legacy code).
  • This style is also used by Rust.

I see two ways in which we can fix this clash of styles.

Fix 1: always use iterable style  

This fix works as follows:

  • We pretend that the new API wraps iterables – loosely similar to how Lodash and jQuery work.
  • Iterator.from() wraps iterables and starts API method chains.
Iteration value Accessing API methods
Legacy iterable Iterator.from(x).take(2)
Legacy iterator Never encountered
Non-iterator iterable (new API) Iterator.from(str).take(2)
Iterable iterator (new API) Iterator.from(map.keys()).take(2)

Pros and cons:

  • Pro: compatible with the status quo

  • Con: relatively verbose

  • Con: The illusion of only working with iterables is broken whenever an iterator method has parameters that are iterators. For example, we may get method .zip() in the future:

    Iterator.from(arr1).zip(
      Iterator.from(arr2),
      Iterator.from(arr3),
    )
    
  • Con: The name Iterator doesn’t help, either.

Fix 2: always use iterator style  

  • We pretend there are only iterators and that iterables don’t exist.
  • API iterators being iterable means that built-in language constructs can handle them. That is, we can pretend that the constructs accept iterators.
  • Iterator.from() means getting a “proper” iterator from a data structure that:
    • either doesn’t support the new API (such as library data structures)
    • or has no convenient method for creating iterators (such as strings).
    • Long-term, this static helper method won’t be used anymore.
Iteration value Accessing API methods
Iterable (legacy) Iterator.from(iterable).take(2)
Iterator (legacy) Never encountered
Non-iterator iterable (new API) Invoke method to create API iterator:
arr.values().take(2)
map.entries().take(2)
Rare exception (*):
Iterator.from(str).take(2)
Iterable iterator (new API) arr.keys().take(2)

(*) Strings need a method for creating iterators that is more convenient than [Symbol.iterator]().

What does that mean for new JavaScript code?

  • Functions and methods should accept iterators, not iterables – especially in TypeScript.

  • If we return a value that supports the iteration protocol, it should be an iterator, not an iterable. This iterator must be an instance of Iterator.

  • We don’t make data structures iterable anymore, we implement methods that return instances of Iterator.

    • The most common current names for such methods are: .keys(), .values(), .entries()
    • .iterator() could also work (due to Symbol.iterator).

The following code illustrates iterator-only style:

// Old: iterate over iterable
for (const element of myArray) {}
// New: iterate over iterator
for (const element of myArray.values()) {}

// Old and new
for (const [index, value] of myArray.entries()) {}

// Old: data structure is iterable
for (const element of myDataStructure) {}
// New: data structure has a method that returns an iterator
for (const element of myDataStructure.values()) {}

// Old: accept iterables
function logData1(iterable) { /*···*/ }
// New: accept iterators
function logData2(iterator) { /*···*/ }

Pros and cons:

  • Con: This style is a break with existing practices.
  • Pro: This style feels simpler than existing practices (there are only iterators, no iterables).
  • Pro: The weird dual nature of generator objects is not an issue anymore:
    • They are allowed to be mostly iterators.
    • It doesn’t matter that there are two kinds of iterables (because we don’t use iterables anymore).

How will this proposal affect future JavaScript APIs?  

Assuming we all agree on iterator style:

  • It looks like upcoming ECMAScript APIs will switch to iterators – for example: The proposed new Set methods have a parameter other and require method other.keys() to return an iterator, not an iterable.

  • Strings need a method for creating iterators that is more convenient than [Symbol.iterator](). Maybe: .toCodePoints().

  • APIs will have to decide what they mean if they require a parameter iter to be “an iterator”:

    • Do they only require that iter.next() exists (“core iterator”) or
    • do they require that iter is an instance of Iterator?

    For method .keys() mentioned in the previous item, the former approach was chosen.

    Consequences:

    • APIs that accept core iterators may profit from for-of and for-await-of accepting core iterators. But maybe only ECMAScript APIs will accept core iterators and non-built-in APIs will only accept instances of Iterator.
    • To express this distinction, TypeScript may have to introduce a new interface called (e.g.) CoreIterator that only has method .next().
  • In TypeScript, interface IterableIterator may not be needed anymore. It is currently the return type of methods such as .keys() (of Arrays, Maps, etc.), so that their results are accepted by language constructs that require their operands to be iterable. However, with the ECMAScript proposal, every Iterator is iterable.

The new API: asynchronous iteration  

The asynchronous version of the iterator method API is similar to the synchronous version but uses asynchronous iteration instead of synchronous iteration.

AsyncIterator.from()  

The static method AsyncIterator.from(x) returns an instance of AsyncIterator:

  • If x is an asynchronous API iterable, it returns x[Symbol.asyncIterator]().
  • If x is an asynchronous API iterator, it returns x unchanged.
  • If x is an asynchronous legacy iterator (that doesn’t support the new API), it wraps it so that it supports the new API and returns the result.
  • If x is an asynchronous legacy iterable, it wraps the result of x[Symbol.iterator]() and returns it.
  • If x is a synchronous iterable (API or legacy), it returns an asynchronous iterator for its values.

Prototype methods that return asynchronous iterators  

The API for asynchronous iterators provides asynchronous analogs of the synchronous iterator methods that return iterators – for example:

assert.deepEqual(
  await arrayFromAsync(
    createAsyncIterator().filter(x => x <= 'b')
  ),
  ['a', 'b']
);
assert.deepEqual(
  await arrayFromAsync(
    createAsyncIterator().map(x => x + x)
  ),
  ['aa', 'bb', 'cc', 'dd']
);

We used these helper functions:

async function* createAsyncIterator() {
  yield 'a'; yield 'b'; yield 'c'; yield 'd';
}
async function arrayFromAsync(asyncIterator) {
  const result = [];
  for await (const value of asyncIterator) {
    result.push(value);
  }
  return result;
}

As an aside: Array.fromAsync() is an ECMAScript proposal.

AsyncIterator.prototype.flatMap()  

The asynchronous iterator method .flatMap() is the only case where not only the return type changes, but also the type of the parameter. Its type signature is:

AsyncIterator<T>.prototype.flatMap<U>(
  mapFn: (
    value: T, counter: number
  ) => Iterable<U> | Iterator<U> | AsyncIterable<U> | AsyncIterator<U>
): AsyncIterator<U>

In other words: The callback mapFn can return iterables or iterators that are either synchronous or asynchronous.

Prototype methods that return Promises for values  

If a synchronous iterator method returns non-iterator values, then its asynchronous version returns Promises for these values. That’s why we use await in line A, B, C and D:

async function* createAsyncIterator() {
  yield 'a'; yield 'b'; yield 'c'; yield 'd';
}

assert.deepEqual(
  await createAsyncIterator().toArray(), // (A)
  ['a', 'b', 'c', 'd']
);
assert.deepEqual(
  await createAsyncIterator().reduce((acc, v) => acc + v), // (B)
  'abcd'
);
assert.equal(
  await createAsyncIterator().some(x => x === 'c'), // (C)
  true
);
assert.equal(
  await createAsyncIterator().find(x => x === 'c'), // (D)
  'c'
);

For looping over asynchronous iterators we can use .forEach() and will often await the empty Promise it returns:

await createAsyncIterator().forEach(
  x => console.log(x)
);
console.log('DONE');
// Output:
// a
// b
// c
// d
// DONE

We can also use for-await-of:

for await (const x of createAsyncIterator()) {
  console.log(x);
}
console.log('DONE');

Implementations of the iterator helpers  

The benefits of the new iterator methods  

Benefit: more operations for data structures that support iteration  

With the new iterator methods, any data structure that supports iteration gains more operations. For example, Sets don’t support the operations filter and map. Thanks to the new iterator methods, they now do:

assert.deepEqual(
  new Set( // (A)
    new Set([-5, 2, 6, -3]).values().filter(x => x >= 0)
  ),
  new Set([2, 6])
);
assert.deepEqual(
  new Set( // (B)
    new Set([-5, 2, 6, -3]).values().map(x => x / 2)
  ),
  new Set([-2.5, 1, 3, -1.5])
);

Note that new Set() accepts iterables and therefore iterable iterators (line A and line B).

Benefit: incremental processing  

One important benefit of iteration is that data consumers that also produce iterated data, process data incrementally. As an example, consider code that reads a text file, puts the string '> ' before each line and logs the result.

If we use an Array, we have to read the whole file before we can log the first line.

readFileSync('data.txt') // hypothetical
.split(/\r?\n/)
.map(line => '> ' + line)
.forEach(line => console.log(line))
;

If we use iteration, we can log the first line shortly after reading it. With the proposed new API, this could look like this:

createReadableStream('data.txt') // hypothetical
.pipeThrough(new ChunksToLinesStream())
[Symbol.asyncIterator]()
.map(line => '> ' + line) // new API
.forEach(line => console.log(line)) // new API
;

We have had generators for this kind of incremental processing for a while. Now we also have the iterator methods.

Further reading