iterfn
A collection of functions to work with iterators similar to Rust's Iterator trait.
Iterators are very useful to work with collections and it's worth knowing that not everything needs to be an array. A very good and detailed explanation of iterators and iterables can be found in Chapter 21. Iterables and iterators of the book Exploring ES6.
Motivation
In JavaScript the most well known iterables are arrays. Whenever you want to use
methods of Array.prototype like map
or filter
, you'd
usually create an array from your data type. For instance a Set object is
iterable, so you would expect it to have the same functions as array. Sadly
that's not the case. This library makes it very convenient to work with
iterables and iterators.
Another great aspect of iterators is their laziness. This makes it possible to use infinite iterators and also allows you to chain multiple methods without having to evaluate all values before reaching the next step in the chain. This also means that you won't allocate a new array after each transformation.
Usage
; const sum = ; // 10 const doubledIter = ;// Iterator needs to be consumeddoubledIternext; // { value: 2, done: false }doubledIternext; // { value: 4, done: false }doubledIternext; // { value: 6, done: false }doubledIternext; // { value: undefined, done: true } const composedIter = ;for const val of composedIter console;// Output:// 8// 7// 6 // Any iterable worksconst map = ;map;map;// Sum keys and values of a Map<number, number>const sum = ; // 15 // Generator functions { 1; 12; 5;}// Calling the generator function creates an iterable iteratorconst count = ; // 3// iter also accepts a generator function directlyconst lastSquare = ; // 25
An object can be an iterator or an iterable or both at the same time. iter
will create an iterable iterator whenever possible, that is an iterator that is
also iterable, so you don't have to worry about distinguishing them.
Using functions directly
It is also possible to directly use the functions that are present on the extended iterators. This removes any overhead of extending the iterators but requires the user to supply a correct iterable. This also makes chaining more cumbersome.
; const regularSum = ; // 10const sumSquaredEven = ; // 20 // Step by stepconst evenNumbers = ; // 2, 4const squared = ; // 4, 16const finalSum = ; // 20
Infinite iterators
It is possible to operate on infinite iterators, but be aware that consumers
require to fully traverse the iterator unless they are short-circuiting. Also
the adapter reverse()
requires to fully consume an iterator as well. The
following examples show how you can operate on infinite iterators, which would
not be possible by just creating an array from an iterator. On the other hand
cycle()
creates an infinite iterator.
; { let i = 1; while true i; i += 1; } const sumFirstTen = ; // 45// First number that is divisible by 2 and 11const divisibleByTwoAndEleven = ; // 22 // Use the composed iterator directlyconst multiplesOfThree = ;multiplesOfThreenext; // { value: 3, done: false }multiplesOfThreenext; // { value: 6, done: false } const smallSquares = ; // [1, 4, 9, 16, 25, 36, 49]
Adapters
An adapter is a function which takes an iterator and returns another iterator.
Two of the most common adapters are map()
and filter()
. In JavaScript these
functions are available for arrays, which return a new array and therefore it is
possible to chain them. All the adapters can be chained as well.
; const evenNumbers = ;const doubled = ; // Advancing the iterator with next()evenNumbersnext; // { value: 2, done: false }evenNumbersnext; // { value: 4, done: false }evenNumbersnext; // { value: undefined, done: true } // Using in a for-of loopfor const num of doubled console; // An iterator which will yield the values 4, 8 and 12const doubledEven = ;
Difference to Array
A notable difference to using arrays, is that iterator adapters create a new iterator without consuming the values of the composed iterators. This means that any side effects in the functions will appear for any value at once when the iterator is being consumed instead of per adapter function. Although such functions should usually not have any side effects, it is still important to know the evaluation order.
; const doubledEven = ; // Consuming the iteratorfor const x of doubledEven console; const array = 1 2 3 ; for const x of array console;
Output:
Iter - filtering 1
Iter - filtering 2
Iter - mapping 2
Iter - result 4
Iter - filtering 3
Array - filtering 1
Array - filtering 2
Array - filtering 3
Array - mapping 2
Array - result 4
The above code is for illustration purposes, if you would like to debug the iterator steps you should use inspect() instead of modifying the map or filter functions directly.
const doubledEvenDebug = ;
Advancing composed iterators
An iterator can only be used once and advancing a composed iterator also
advances the underlying iterators. It is also possible to advance the underlying
iterators directly, without triggering the composed one. Keep in mind that
arrays are iterable but not iterators, where [Symbol.iterator]()
creates an
iterator from the array.
; const arr = 1 2 3 4;const arrIter = arrSymboliterator;const doubledIter = ; doubledIternext; // { value: 2, done: false }doubledIternext; // { value: 4, done: false }arrIternext; // { value: 3, done: false }doubledIternext; // { value: 8, done: false }doubledIternext; // { value: undefined, done: true }arrIternext; // { value: undefined, done: true }
Consumers
A consumer is a function which takes an iterator and consumes it to return
a value. They are the last method called in a chain of iterators. The most
common consumer for arrays is reduce()
which for instance can be used to sum
all values.
; const sumReduce = ; // 10const sum = ; // 10const sumEven = ; // 6const max = ; // 13
Some consumers seem rather useless for arrays, but are very nice to have for
other iterables, that don't have properties like length
.
; { 1; 12; 5;} const length = ; // 3const second = ; // 12, index starts at 0const sum = ; // 18const last = ; // 5
Collect
collect()
is a special consumer which creates a collection from an iterator.
By default it creates an array, but it accepts a function that will be used to
create such a collection. An array can easily be created with the spread
operator, without using this function. But it can definitely improve the
readability of a long iterator chain.
; { 'hello'; ' '; 'world';} const array = ; // ['hello', ' ', 'world'] { let str = ''; for const val of iter str += val; return str;}const string = ; // 'hello world' const longChain = ; // [4, 2, 5, 5] // Not immediately obvious that it creates an array with the iterators values,// especially when just seeing the end, even without using complex blocks.const longSpread = ... ; // [4, 2, 5, 5]