In this post, I explain why Iterables and AsyncIterables in JavaScript are great and why the library I released—iterated—is a great tool to unleash their potential. It is time to move away from only Arrays into Iterables.
Simply said, an Iterable is any data or variable that we can loop.
This
includes String,
Array,
Set,
and
Map.
Usually data is in arrays, so we use iterables already "under the hood"
even if we don’t know it.
For example,when using
:for..of
Javascript introduced iterables a while ago,
and since 2017 is widely supported.
Other languages
like Python had it since 2001, and they are a core part
of the language.
However, in my experience, developers tend to focus on working only
with arrays
(e.g.,
Array.map,
Array.filter)
instead of shifting their mind and working with iterables,
even if iterables have several benefits:
- Abstraction over the collection where we contain our data.
- Avoiding intermediate data structures, reducing CPU and RAM usage, and allowing infinite and streaming collections.
- AsyncIterators, putting promises into iterables to iterate with I/O and API calls.
- They are a great fit with pipes.
In this post, I plan to entice you to move from an Array-centric vision to an Iterable-centric one.
Abstraction
The first benefit is abstraction. As we have several types of collections, like Array, String, or Map, iterables allows us to make agnostic functions that can work with any of these collections. Or said in another way, functions that need only something that loops:
Avoiding intermediate data structures
Transforming arrays of data means in many ways creating intermediate data structures that cost RAM and CPU.
In the following example, Array.map
and Array.filter
create new arrays, so we now have three arrays: input
, x
, y
.
Iterables do not generate an intermediate data structure:
This is because iterables are lazy: they do not iterate until necessary, only on
demand.
For example, when a function forces them to generate an array.
In sum, with iterables we avoid intermediate data structures, which is crucial when working with big datasets.
Generators and yielding
The same way that arrays and strings are now iterables, functions
can be iterables too. Rephrasing it: we can iterate functions.
To tell JavaScript that a function can be iterated, we use
two things: the
:yield
keyword and an asterisk after function
For a function accepting an Iterable (like the printValues
we defined
earlier) this is still a regular Iterable:
We call these iterable functions generators.
A function that yields avoids creating intermediate data structures. We
can demonstrate this by comparing the implementation of a regular Array.map
and a yielding map
functions:
Even if both functions achieve the same:
- The Iterable one allows any type of iterable (i.e.
Array
,String
,Set
,function*
) - The Iterable one avoids creating an intermediate data structure (i.e.
newArray
).
And thus yieldingMap
is superior to regularOldMap
.
iterated provides functions such as
yieldingMap
(i.e., map
)
to handle iterables.
Finally, thanks to their lazy nature, generator functions enable collections
of infinite values, such as computing the fibonacci sequence or streaming
information.
AsyncIterators
Converting the yieldingMap
example to an async function
(e.g., for fetching data) transforms the return from an Iterable
to an AsyncIterable
:
This means that in every loop of AsyncIterable
it has to await
:
The beauty of this approach is that we only loop after fetching a user (i.e., lazily).
We can achieve the same without using AsyncIterable
but increasing
the code boilerplate:
As JavaScript’s async/await is everywhere, having a simple way to handle async calls helps to reduce code and complexity.
AsyncIterables are specially useful when we do not know at the moment
of starting the loop when it is going to be done, for example when
accessing I/O
.
An issue is that the same function cannot handle Iterable
and AsyncIterable
(because AsyncIterable
requires to be inside async
functions and
for ... of
loops), so
we partially lose the value of having a function that just works with
any kind of collection of data.
iterated fixes this by auto-handling
Iterable
and AsyncIterable
transparently:
pipes
When processing data, pipes increase legibility by focusing on the
operations to do to over the data, instead of constructors like
.for...of
Pipes are a natural fit for iterables because they both focus on the data and the operations we want to apply on them. The following example is what I use to load the posts of this blog:
Executing the previous example still does not trigger loading any posts, it only happens when calling an operation that returns an actual data structure.
Note how the function fetchPost
only fetches a single post,
ignoring whether it is used in a collection, thanks to map
.
A usual problem with pipes is following the shape of the data, specially after applying several transformations. That is why I ensured that Iterated brings TypeScript support, inferring the data type in each step. For example, the following causes TypeScript to complain:
About the built-in methods
When handling Arrays, JavaScript’s built-ins use chaining instead of piping:
And the JavaScript team is repeating it with the proposal for built-in iterable methods:
Although both methods work, pipe
has an advantage over chain
:
it is functional.
Thanks to this, we can easily allow to use AsyncIterable
and,
most importantly, it is extensible.
By extensible, I mean adding your functions doesn't feel alien or quirky:
And finally, a word of warning with the built-ins as they don't work for all iterables, which iterated does.
Conclusions
Iterables and AsyncIterables are an improvement
when working with JavaScript, as they abstract us from collections
(e.g., Array, Set, Map, function*
) and focus on our data, they iterate
only once, and they reduce RAM and increase performance.
Issues are changing our mental process and functions to iterables,
or embrace pipes.
Pipes are a good way to transform data with iterables as they focus on the transformations we want to do to our data. Although there is a JavaScript proposal for iterable chaining, I believe pipes are more agnostic and extensible, especially when moving between Iterable and AsyncIterable, and supporting any kind of iterable.
Moreover, although there are many good libraries to work with iterables (e.g., iterate-iterator, iterare, the new built-in methods) only iterated brings all the following:
- Pipes.
- Great typing support.
- Iterable and AsyncIterable transparent support.
- Simple extensibility.
- Simple interface.
As its biggest drawback, iterated is new and would benefit from more and improved functions.
What do you think? Am I convincing you on iterables (and Iterated)? Are there any goodies, drawbacks, and packages I missed? I would love to read your comments in Mastodon.