Monads simplified with Generators in TypeScript: Part 1
Async functions solve callback hell for the Promise “monad”. Generators solve this for any Monad in TypeScript.
--
A couple of weeks ago, I decided I wanted to learn Monads in depth. I am interested in functional programming and prefer to keep functions pure. I try to keep variables immutable and use algebraic data types (tagged unions) to model my data in Typescript. Although learning about Monads can certainly be interesting and challenging, I expected them to be of limited use in my day to day work as a TS developer.
This expectation slowly changed. Apparently, I already use “monad-ish” interfaces every day. You too might already chain callbacks in the then
method of a Promise
. Moreover, you may also chain callbacks with the .map
and .flatMap
methods of Array
. If so, you are already using Monads!
After a workshop about the ZIO library in Scala, (thanks Mark de Jong!), I realized that there are many other useful Monads. They are meant to solve the same problems I encounter every day as a professional TS developer. However, Monads solve them more elegantly than I ever dared to dream about! In the upcoming parts of this series, we will go over Monads such as Reader
and Either
that solve dependency injection and error handling respectively. All this in a pure, elegant, and typesafe way.
But first, let’s get a deeper understanding of the problem that Promise
and and async/await
solve in JavaScript and why this similar to the problem that Haskell tried to solve when they implemented Monads in the '90s.
How the Promise monad rescued JavaScript from callback hell
If you have written JavaScript before Promise
became standardized in ES2015, the following code will probably look familiar to you. Callbacks are a natural way to handle asynchronous code in JavaScript. In the browser, it allows you to do an HTTP request without blocking new user interactions. In Node, it allows database access without blocking new HTTP requests from being handled.
Passing a callback as a parameter like in the example above solves this problem. Unfortunately, it often becomes so unreadable it is referred to as callback hell.
Luckily, Promise
became standardized. The trick is to have a method of the following form:
This allows the code above to be chained using the then
method:
Much better! Besides that, async functions were introduced in ES2017. This allows writing code without passing callbacks at all.
For this example, it probably depends on your taste, which style you prefer. If you are comfortable with functional programming, you may actually use the then methods. You could even write it in one line if you are into point-free programming:
However, there are many cases where using the async/await
syntax is essential to solving callback hell:
If the variable a
is needed in every subsequent callback, then those callbacks need to be wrapped by the callback of a
. A similar problem holds for the other variables, which leads again to callback hell. Luckily, async functions can rescue us here:
This is syntactic sugar for the code above, but helps a lot with writing complex asynchronous flows!
What have Promises to do with Monads?
The problem of callback hell is often associated with asynchronous programming in JavaScript. However, in Haskell, they found that this problem has not so much to do with asynchronous programming per se and that similar problems arise whenever you have a structure that they called a “Monad”.
To define the Monad
interface, you need higher kinded types, which Typescript doesn’t support. And even if TypeScript would support that, it would miss Haskell’s type classes that allow interfaces to be implemented by existing types, such as a Promise
or an Array
.
It is possible to imitate those advanced type patterns in TypeScript, but in this post, we will keep it a bit more simple and just see a Monad as a type that has certain properties. We say that a type Monad<A>
is monadic for a type A
if we can define the following functions:
You can think of this that there is a way to put a value: A
inside a monad with the function of
. And there is a way to access this value of type A
again by passing a callback to the flatMap
function.
For a Promise
those functions are implemented by Promise.resolve
and by the then
method:
The Monad laws
In Haskell, a Monad must not only implement the of
and the flatMap
function, it must also obey the following 3 laws.
The identity laws make sure that of
and flatMap
work as essentially as inverses of each other.
The left identity law says that whenever you put a value inside a monad with of
and access that value again by passing a callback toflatMap
, then you will have the same value available in the callback again. For a Promise
, you can see that as:
Makes sense? We wouldn’t want to have the then
method magically transform the value we are passing, for example, that we put in 3
, and get out 4
!
The right identity law says that whenever you access the value of a Monad using flatMap
, and then put that value in a Monad again with of
, you will have back the same Monad.
For a Promise
, you can see that as:
Last we have the associativity law:
For Promise
’s, you can see this as that it doesn’t matter if you chain then
methods inside the callback of another then
method, or that you chain it outside of the callback on the resulting promise.
But why?
I hope this gives a sense of why a Promise is Monad-ish, but there is a caveat here. Promises are designed so that they are automatically flattened. You can’t have a Promise inside of another Promise:
But according to the left identity law, x
must be equivalent to Promise.resolve(3)
. The monad rules don’t allow that, and for good reasons. We saw thatasync/await
syntax allow for using Promise’s without writing callbacks. In Haskell, you have a similar syntax (called do
-notation) that does essentially the same, but then for any Monad! The laws are essential to make such a generalized syntax predictable.
In essence, you could say that Monad-ish structures arise whenever it makes sense to do calculations by accessing the value in a callback instead of using the value directly. Here are examples of Monad-ish structures that are available in TypeScript and the problems they solve:
- In the
Promise
monad you use a callback, as if you would access the value directly, it may not be there yet. - An
Array
is a monad where working with callbacks allows transforming the values without mutating anArray
in a for loop - An
Optional
is a Monad that isn’t directly available in TypeScript, but you can think of it as being similar toNullable<T> = T | null
whereT
is notnull
itself. Here callbacks allow for accessing the value without having to do a manual null check. - An
Iterable
can represent a sequence of infinitely many values in JavaScript. By using callbacks you can transform it into another infiniteIterable
without problems while looping over it one by one would take infinite time!
To see those types as Monad’s, you have to implement the of
and flatMap
function and show that the Monad laws hold. For an Array
, the monad functions can be implemented as follows:
We have special async/await
syntax only available for Promise
’s in JavaScript. Callback hell arises when you compose a Monad from multiple other Monad’s that depend on each other, which is a very common pattern for Promise
’s.
However, it is possible to have the exact same callback hell for Array
’s. For example, say that we want to construct right triangles with sides a
and b
, and hypotenuse c
.
And there we have callback hell again! To avoid callback hell for any Monad, Haskell came up with a special do-notation syntax that “looks” like accessing the monadic value directly, but is just syntactic sugar for a chain of flatMap
’s:
Maya Victor makes a very similar point as I draw here in this post, and says that “if mapping over nested arrays was as common as dealing with asynchronous values, by 2018 we’d probably have an multi/pick
syntax”. The multi/pick
syntax for Array
’s would look like this:
Generators can solve callback hell for any Monad in TypeScript
However, this “imaginary” syntax is actually possible in TypeScript with Generator
s!
The idea is essentially the same as with async
functions. Instead of using await
to access the value of type A
of Promise<A>
, we use the yield
keyword to access the value of type B
of any Monad<B>
.
Using yield
directly on a Monad works fine if you don’t care about types. For TypeScript, we need to wrap the Monad in an extra generator function, pick
in the example above, to infer the type that is yield
ed.
The multi
function in the Array
example accepts a Generator
and will essentially rewrite the generator function as the nested chain of flatMap
’s and give back the resulting Array
. Because we don’t have higher kinded types in TypeScript to specify a Monad interface, it is not possible to make this syntax automatically work for any Monad in a typesafe way. However, it is possible to manually implement such multi
and pick
functions Monad-by-Monad, so that you can use similar typesafe Generator
syntax for any Monad.
In the next part of this series, we will go over how Generator
’s exactly work. This Generator
syntax may not often be useful for Array
’s, but it helps a lot for the Reader
monad (which solves dependency injection using pure functions) and the Either
monad (which helps with doing error handling in a typesafe way). For those two monads, the Generator syntax makes sense for the exact same reason it makes sense for Promise
’s: you often need to compose one Monad from multiple other Monads that depend on each other.
In the meantime, if you are interested in the implementation of multi
and pick
above you can check how it was implemented in the following code sandbox: https://codesandbox.io/s/multi-pick-syntax-gt65u?file=/src/index.ts
There is also a ZIO inspired library available for TypeScript that tries to imitate higher kinded types in TypeScript which has Generator syntax available for Monads specified in that way: https://github.com/Matechs-Garage/matechs-effect
I hope you learned something new, and until next time!