This story shows Generators as an explicit but seamless syntax for programs with asynchronous operations, shared mutable state and other side effects. The conversion is based on so-called Monads.
Other researchers were designing practical languages with only pure functions. Using only pure functions makes programs more verbose and harder to read. Monads were applied as a practical tool for converting programs with effects into pure ones. Here is a citation from one of the best monads tutorial — Monads for functional programming by Philip Wadler (1995):
It is with regard to modularity that explicit data flow becomes both a blessing and a curse. On the one hand, it is the ultimate in modularity. All data in and all data out are rendered manifest and accessible, providing a maximum of flexibility. On the other hand, it is the nadir of modularity. The essence of an algorithm can become buried under the plumbing required to carry data from its point of creation to its point of use.
Doesn’t it sound familiar? E.g., property drilling for React Components and one of the reasons for state management to solve it.
Original abstract do-notation is a macro converting programs looking like imperative one into abstract API calls. The concrete implementations of that APIs can construct an Iterable object, or a Promise or many other things. This way the same syntax (and the same programs) can be reused in different contexts.
There are many examples of using generators for async code so I’ll illustrate the idea on another case instead. It is maybe not so practical, but we can turn a generator function with a mutable state into a pure function.
Here both functions
incrX2 have side effects. They change and read shared data. But the resulting function
state(incrX2)is pure. The function
state does actual conversion between Iterable into a State monad.
This is how it looks with inlined framework functions:
The example skips abstract API layer. There are quite a few options to chose its basis, but the most simple one is two functions:
chain. They both return monadic(effectful) value. It is an abstract thing and can be anything depending on the concrete API implementation. For the abstract interface, the values of this type are entirely opaque.
of— takes any value and returns effectful value, what it does exactly with the argument is defined by interface implementation
chain— takes some effectful value and a function mapping anything to another effectful value, returns some other effectful value
The concrete implementations of the function can do anything if this conforms to so-called monadic laws. In fact, my choice of
Here are the laws:
(f, x) => chain(of(x), f)should be equal to
(f, x) => f(x)
m => chain(m, of)should be equal to
(m, f, g) => chain(chain(m, f), g)should be equal to
(m, f, g) => chain(m, x => chain(f(x), g))
If the laws hold we can use the API in do-notation, or in some abstract functions working for any monad.
Let’s convert Iterable into this abstract interface. It is almost the same like for State except the functions are replaced with abstracted calls.
For State, effectful value is a function taking some original state and returning a pair of a resulting value and a new state value. Here is State monad definition using the abstract intermediate layer:
The first monad law doesn’t work for Promises, it is fine for practical purposes though.
Considering generators are enough for Promises and Iterators one may wonder why we need Async Generators. Indeed it is pretty easy to convert monad combining the two effects from plain generators. However, there won’t be any replacement
for await-of statement. It is yet another syntax sugar for traversal.
Coroutines/Iterables cannot define be converted to any monad. For example, Observable is a monad, but generators cannot be used as a do-notation for them.
Generators syntax won’t work because
fun may be called more than once.