Javascript generators and functional reactive programming
tl;dr: ECMAScript 6 introduces a language feature called generators, which are really great for working with asynchronous code that uses promises. But they do not work well for functional reactive programming.
ES6 generators allow asynchronous code to be written in a way that looks synchronous. This example uses a hypothetical library called Do (implementation below) that makes promises work with generators:
It is possible to use generators now in Node.js version 0.11 by using the
--harmony
flag. Or you can use Traceur to transpile ES6 code with
generators into code that can be run in any ES5-compatible web browser.
I recently saw an informative talk from Jacob Rothstein on Co and Koa. Those are libraries that make full use of generators to make writing asynchronous code pleasant. The implementation of the above example in Co is almost identical.
Co operates on promises and thunks, allowing them to be expanded with
the yield
keyword. It has nice options for pulling nested
asynchronous values out of arrays and objects, running asynchronous
operations in parallel, and so forth. One can even use try
/ catch
to catch errors thrown in asynchronous code!
Co is specialized for asynchronous code. But when I look at it I see something that is really close to being a monad comprehension - very much like the do notation feature in Haskell. With just a little tweaking, the use of generators that Co has pioneered can almost be generalized to work with any kind of monad.
For example, libraries like RxJs and Bacon.js implement event streams, which are a lot like promises, except that callbacks on event streams can run more than once. This example uses Bacon to manage a typeahead search feature in a web interface:
The idea is that when the user enters more than two characters of text into a search box, a background request is dispatched and search results appear on the page automatically. This example requires a library that is a little more general than Co, that is able to operate on Bacon event streams in addition to promises and thunks. Here is a basic implementation of that library:
This is the function that makes the example at the top of this post work. It is
a simplified version of what Co does - the difference being that Do delegates to
a flatMap
function to handle yielding. We just need an implementation of
flatMap
that can operate on some different monad types. The one above works
with promises or with Bacon event streams. An implementation that is easier to
extend would be nice; but I will leave that problem for another time.
Unfortunately, the Bacon example does not work. Streams - unlike promises - get
many values. That means that the code in each generator has to run many times
(once for each stream value). But ES6 generators are not reentrant: after
resuming a generator from the point of a given yield
expression, it is not
possible to jump back to that entry point again (assuming the generator does not
contain a loop). With the Bacon example, after the first keyup
or change
event the search result list will just stop updating.
Getting synchronous-style functional reactive programming to work well would require immutable, reentrant generators. ES6 generators are stateful: every invocation of a generator changes the way that it will behave on the next invocation. In other words, a generated is mutated on every invocation:
g = // initialize the generator
.value === 1
.value === 1
.value === 3
.value === undefined
An immutable implementation would return a new object with a function for the next generator entry point, instead of mutating the original generator:
g1.value === 1
g2.value === 2
g3.value === 3
// We can go back to previous entry points.
.value === 2
.value === 1
An immutable generator could be implemented with some simple syntactic transforms. The basic case:
would transform to:
The next
property is returned is a generator that is used to invoke the next
step. In this view generators are just functions. Not only that - a generator
is a closure that has access to the results of previous steps via closure scope.
There would just need to be a few cases to handle appearances of yield
in
a return
statement, a try
-catch
block, or as its own statement. With that
kind of stateless design, the Bacon example would work fine. But as far as
I know, there is no plan for stateless, reentrant generators in ECMAScript.
It is possible to make functional reactive programming work with non-reentrant generators by using loops in the generators. This approach is not as general or as composable. Asynchronous pieces would have to be declared specially at the top of the function, for example.
Notice that yield
is overloaded to accept asynchronous values and to return
results - which requires some awkward logic to inspect generator values.
I do not know how to implement resultView
as a loop, since it requires
combining two event streams: search queries and JSON responses. I do not see
any advantage of loops in generators over asynchronous-style callbacks. But
maybe someone more imaginative than me can come up with a more elegant solution.
Revisions
2014-08-04 —
Fixed broken link, fixed incorrect indentation in code snippets.
2014-08-05 —
The hypothetical generators that I describe are immutable, not stateless. Thanks to Blixt for pointing out the distinction.