Welcome to the second post in the series. In the first one, you had a chance to build your first monad in TypeScript. In this post, you’ll see how to take advantage of generators to make the monadic code more readable.
You can find all the code from the series in this repository. Check out different branches for code relevant to the specific part of the series.
Generator functions
Generator functions have been introduced to JavaScript as part of the ES6 standard. They are a special case of functions where it’s possible to pause execution in the middle of the function body. This might sound counter-intuitive, especially if you consider the fact that JavaScript is single-threaded and follows the Run-to-completion approach. However, with generators, the code is still executing synchronously. Pausing execution means giving the control back to the caller of the function. The caller can then resume execution at any point. Let’s see a simple example:
1 | function* numbers(): IterableIterator<number> { |
There are two pieces of new syntax here. Firstly, there is a * following the function keyword. It indicates that numbers is not a regular function but a generator function. Another new thing is the yield keyword. It’s a bit like return but it can be used multiple times inside the function’s body. By yielding a value, the generator returns a value to the caller. However, unlike return, the caller may decide to resume execution and give control back to the function. When it happens, the execution continues from the latest yield. First, we need to invoke numbers to get a generator instance.
1 | const numbersGenerator = numbers(); |
At this point, not a single line of the numbers function has been executed. For this to happen, we need to call next. It will start executing the function until it encounters the first yield. At this point, the control will be given back to the caller.
1 | console.log('Outside of numbers'); |
When we run these lines, we will get the following output:
1 | Outside of numbers |
As you can see, the execution jumps back and forth between numbers and its caller. Each next call returns an instance of IteratorResult which contains the yielded value and a flag done which is set to false as long as there is more code to execute inside numbers.
Generators are a very powerful mechanism. One of their uses is building lazy, infinite sequences. Another one is co-routines – a concurrency model where two pieces of code can communicate with each other.
Maybe implemented with generators
As a reminder, we implemented the getSupervisorName function in the following way:
1 | function getSupervisorName(maybeEnteredId: Maybe<string>): Maybe<string> { |
Obviously, the code is more readable than the original version which involved three levels of nesting. However, it looks very different from regular, imperative code. Can we make it look more like imperative code? As we know, generators allow us to pause execution of a function so that it can be later resumed. This means that we can inject some code to be executed between yield statements of given function. We could try to write a function that takes a generator function (a function with some yield statements) and injects the logic of handling empty results. Let’s start by writing an implementation of getSupervisorName that will use yield statements to handle empty results.
1 | function* () { |
Let’s assume that maybeEnteredId is defined in the function’s clojure and that it’s type is Maybe<string>. Now, the semantics of const enteredIdStr = yield maybeEnteredId is:
- if
maybeEnteredIdcontains a value then assign this value toenteredIdStr - otherwise, the whole function should return
None
In other words, yield works exactly like flatMap, but the syntax is very different. It’s much more familiar to an imperative programmer.
Implementing run
This is not over yet. We still need a function that will be able to consume this generator function. In other words, we need something that will give meaning to all these yield statements. We’ll call this function run. It will take a generator function and produce Maybe instance containing the result of the computation. Let’s start with an imperative implementation of run:
1 | static run<R>(gen: IterableIterator<Maybe<R>>): Maybe<R> { |
- The
runfunction accepts a generator functiongen. This function describes our computation. - We enter an infinite loop and call
gen.next(lastValue). This call will cause control flow to entergenand execute until the firstyield(ignorelastValuefor now). - Once this happens, the control flow returns to
run. The value passed toyieldis wrapped insideIteratorResultand returned as a value ofgen.next, resulthas adoneflag. It indicates whether control flow insidegenhas reached the end of its body (i.e. whether there’s more code to execute).result.valueholds the value returned byyield. It’s an instance ofMaybe. Therefore, we check if it’s an empty result (aNone). If this is the case, wereturnaNonefrom the whole computation.- Otherwise, we unwrap our
Maybeand assign the inner value tolastValue. - The loop repeats. However, this time
lastValueis not empty. It will be passed togenas a result of callingyield.
There’s quite a lot going on here. A good way to understand this is to think about two pieces of code communicating with each other.
gensends aMaybe<T>instance torunby callingyield mrunreplies with an instance ofTby callinggen.next(lastValue)
The whole point of this is that we can hide the logic of empty result handling inside run. From the caller’s perspective, it will look like this:
1 | function getSupervisorName(maybeEnteredId: Maybe<string>): Maybe<string> { |
What we’ve achieved is a clean and elegant implementation of getSupervisorName with all the boilerplate hidden in run. However, contrary to the flatMap solution, this code looks more intuitive to an imperative programmer.
Back to monads
That’s nice, you might say, but what does it have to do with monads? We’re not taking advantage of the fact that Maybe is a monad. Let’s fix that. You might’ve noticed some similarity between run implementation and flatMap. Both implementations have to deal with empty results and apply a similar logic: if a Maybe instance is empty then return early with a None. Otherwise, continue the computation with the unwrapped value. It turns out that we can implement run using flatMap!
1 | static run<R>(gen: IterableIterator<Maybe<R>>): Maybe<R> { |
This recursive implementation is much more in the spirit of functional programming.
- We define
stepfunction which takes an optionalvalueand passes it togen.next. As we know, this will cause execution to resume insidegen, up to the nearestyield. - We investigate
result.done. If it’s false (there is still some code to execute), we simply callflatMaponresult.valueand recursively passstepas continuation function.flatMapwill take care of an empty result. - As long as we’re not dealing with a
None, the recursive call tostepwill rungenup until the nextyield. And so one, and so one.
The client code looks exactly the same.
Summary
In this post, we’ve looked at how generators can be leveraged to improve the experience of using monads. They make monadic code even cleaner and, what’s important when working in teams, easier to understand to programmers with imperative background (i.e. the vast majority). The idea of using generators in such a way is the basis of the async/await mechanism. As you’ll learn in the future posts, Promise is also a monad and async/await is nothing more than a specialized variant of function*/yield. But let’s not jump ourselves ahead 🙂 Did you find this approach interesting? What is a more readable way to write monadic expressions – with or without generators? Share your thoughts in comments below!
Did you like this TypeScript article? I bet you’ll also like my book!
⭐️ Advanced TypeScript E-book ⭐️

