ECMAScript ("JavaScript" for the uninitiated)1 catches quite a bit of flak for numerous reasons online, but it can boast some truly spectacular expressiveness, possibly somewhat unmatched when paired with savvy TypeScript sauce that would make a Rustacean blush.
You've no doubt come across the destructuring syntax to effectively return multiple values from a single function, but we can do even better: we can return multiple times from the same invocation.
Possibly mad yet patently beautiful, let's explore the generator.
⚓ Not just multiple values
Introduced in ECMAScript 2015
(ES6), the generator function*
lets you yield values on demand, rather than computing them all at once.
It works with the introduction of a new keyword, yield, which pauses and
resumes the function execution. The one practical difference between an
iterator (what the generator constructs) and a regular
collection (like an Array) is that values are only computed when they're
requested: you may generate an infinite sequence of values, for instance.
⚓ Defining a generator
A generator function is declared using the function* syntax. Inside, the
yield keyword is used to emit values one at a time:
You may also use the function* expression:
Or even extract its constructor to create such functions dynamically:
;
There is however no equivalent arrow function syntax for generators.
⚓
Under the hood: an iterable iterator
Calling a generator function returns an
iterator
object. Well, it even is an iterable iterator, like most will be.
⚓
The iterator protocol
An iterator is an instance providing a next() method that returns an object
with two properties:
value, the yielded value, anddone, a boolean indicating whether the generator has completed.
Technically, both properties are optional: you may very well yield undefined
at any time, and the omission of done is equivalent to having it be false.
An iterator is fairly simple to use: call its next() method to get the
subsequent value; when done is true, the iterator is exhausted.
In the case of a generator function*, the first call to next() executes the
function until the first yield. Subsequent calls to next() will resume
execution until the next yield instruction, or until the function returns (or
finishes), at which point it'll systematically reply with done and no further
values are produced.
// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: false }
// { value: undefined, done: true }
// You may keep calling next() after completion:
// { value: undefined, done: true }
Note that the value after exhaustion is typically undefined, unless you
explicitly return a value from the generator; however, know that this final
value is disregarded entirely by standard API constructs such as for..of loops
and the spread operator:
// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: false }
// Here, the final 'tada' is provided the first time *done* is true:
// { value: 'tada', done: true }
// Subsequent calls yield *undefined*:
// { value: undefined, done: true }
// The final 'tada' is *ignored*:
... // 1 2 3
return value is ignored by consumers of iterable... Which makes for a neat segue into the next section.
⚓
The iterable interface
An iterable is an instance providing a [Symbol.iterator]()2
method that returns an iterator. This is essentially JavaScript's take on
composition, a superior alternative to inheritance.
In practice, the iterator protocol is virtually never implemented without also
providing an iterable interface, as most (all?) standard syntaxes and APIs
are designed to work with iterables rather than raw iterators.
Generator function*s return values that are iterable. As such, you can (for
example) use them in for..of loops:
for of
Note that Arrays, Strings, Maps, Sets, and possibly other built-in
types also implement the iterable interface3.
for of
for of 'abc'
for of new Set
for of new Map
ES6⚓
Not just for..of
With ES6 also came a slew of new syntaxes and APIs designed to work with
iterables, including but not limited to:
-
The spread operator (
...), which expands aniterableinto individual elements. You can use it to pass values as separate arguments to afunctionor to create anarray literalwith it:... // 6 -
The assignment destructuring, which also may work over
iterablesto declare and initialise multiple values at once:// a = 1, b = 2, c = 3 -
The rest parameter syntax, which collects multiple values into an array:
1, 2 // [1, 2] ... // [1, 2, 3]
There is still some more brought by ES6 with regards to integration with
iterables, as well as a lot more general, unrelated goodness that came with
it, but I should bring this article back on topic and to an end: how about the
practical use of generator functions in the wild?
⚓ A practical example
Because their values are, as their name would heavily suggest, generated on
demand, you can use generators to create, for instance, infinite iterators:
Alright, it's good to know, but I was only messing with you: I'm yet to develop
the need for an infinite sequence that isn't served just as well by a simple
mutable number counter or some other mechanism entirely.
There was however still one
time that I
figured would call for a generator function*. It wasn't the code that ran the
fastest, but the most elegant4 one—though I welcome challenges to that
claim.
It was implementing a solution to Reshape the
Matrix, LeetCode's challenge #566, despite the typo
in my PR referring to #556, which reads:
In MATLAB, there is a handy function called
reshapewhich can reshape anm x nmatrix into a new one with a different sizer x ckeeping its original data.You are given an
m x nmatrix mat and two integersrandcrepresenting the number of rows and the number of columns of the wanted reshaped matrix.The reshaped matrix should be filled with all the elements of the original matrix in the same row-traversing order as they were.
If the
reshapeoperation with given parameters is possible and legal, output the new reshaped matrix; Otherwise, output the original matrix.
Here's my generator-based solution (revisited for brevity and clarity):
Don't take my word for it: try for yourself to implement this solution;
chances are it's only then that you'll best appreciate the generator
function*.
Though nowadays, for this specific example, I might rather reach for
Array#flat instead which, contrary to a naive belief, actually performs
better.
And don't hesitate to share with me if you ever come across a legitimate use for JavaScript's generators: I would love to know what you're tinkering with.
-
ECMAScript is the standard, the specification; JavaScript is the language built on that standard, plus some extra features (such as Web
APIs, like theDOM), for browsers. ↩ -
Symbols are special, unique identifiers in JavaScript, also introduced inES6: maybe they deserve an entry of their own sometime. ↩ -
Note that plain
Objects do not implement theiterableinterface, and are broken down withObject.entries(),Object.keys(), orObject.values()instead. ↩ -
Elegance is subjective, but 3 measly statements with no mutable variables has got to beat alternatives! In reality, these acrobatics could otherwise be replaced by
Array#flat, though that one only arrived withES10. ↩