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
.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
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
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.
Promise became standardized. The trick is to have a method of the following form:
This allows the code above to be chained using the
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?
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
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
Promise those functions are implemented by
Promise.resolve and by the
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
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 to
flatMap, 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
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.
Promise, you can see that as:
Last we have the associativity law:
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.
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 that
async/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
Promisemonad you use a callback, as if you would access the value directly, it may not be there yet.
Arrayis a monad where working with callbacks allows transforming the values without mutating an
Arrayin a for loop
Optionalis a Monad that isn’t directly available in TypeScript, but you can think of it as being similar to
Nullable<T> = T | nullwhere
nullitself. Here callbacks allow for accessing the value without having to do a manual null check.
Iterablewithout problems while looping over it one by one would take infinite time!
To see those types as Monad’s, you have to implement the
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
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
b, and hypotenuse
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
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
The idea is essentially the same as with
async functions. Instead of using
await to access the value of type
Promise<A>, we use the
yield keyword to access the value of type
B of any
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
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
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
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!