Generic iteration method idea with example implementation and tests

# Renki Ivanko (9 years ago)

ES5 added the Array iteration methods for iterating over, transforming and reducing arrays, which arguably offer benefits in readability and description of intent, but there are certain pitfalls with applying the methods to the other collection types, which have increased in ES2015 with the addition of new types like Map and Set. I've made a table as illustration with the different methods of looping or iterating over objects and the minimum 'plumbing' required: goo.gl/71dQsG

It's apparent that the for..of loop syntax is the most uniform approach to traversing collections, while trying to use the Array iteration methods requires additional steps and, in the case of iterable objects, requires transforming them to arrays, which loses the benefits of the lazy iteration. Usage of the for..of loops also varies depending on whether entries (key-value pairs) or values are used. A solution would be to offer the familiar semantics of the ES5 Array iteration methods that would support the existing collection types, and also allow defining support for new types. I've made an example implementation of this idea: slikts/esnext-generic-iteration/blob/master/src/iteration.js (the other files in the repository implement reconstructing object types, polyfilling native support and a basic test suite).

The implementation works by adding two new Symbol properties to the iterable objects: @@shape and @@reconstruct. Shape can be one of the values of @@shape.indexed or @@shape.entries. The generic iteration methods use the shape properties to normalize the return values of the iterators, allowing the same signatures for methods and callbacks to be used for all the various types. The @@reconstruct property returns a method that returns a reconstructor object that has a method for accepting a value and an optional key, and a property with the resulting collection, whose type is based on the @@species property of the constructor. This allows for the same behavior as with subclassed native arrays, where the output of the iteration methods will be constructed based on the @@species property. If the iterable object does not support the reconstructor protocol, the result is an Array object.

Some code examples:

map([1, 2], x => x + 1) // -> [2, 3]

filter('abc', x => x !== 'b') // -> ['a', 'c']

map(new Map([['a', 1], ['b', 2]], (v, k) => v + k) // Map { 'a' => '1a', 'b' => '2b' }

The method and callback signatures in this implementation match those of the Array iteration methods.

An argument for the desirability of generic iterator methods is the inclusion of similar methods in utility libraries like lodash that deal with collections.

Additional thought: this approach makes it very simple to define an iterable object like has previously been proposed with Dict.

# Renki Ivanko (9 years ago)

The methods could be added to the Object constructor and used like so:

const {map, filter, reduce} = Object
# Renki Ivanko (9 years ago)

To clarify, the few currently implemented methods are just to demonstrate the idea; the full implementation should have the other methods like every() to allow breaking the iteration and forEach() for side-effects.

Regarding the mention of lazy iteration, the point is that Array.from(), which is required to use the Array iteration methods on other types, transforms the iterator upfront, while, with a generic every() method, it would be possible to process only part of the iterable before breaking.