Pragmatic Monad Understanding

Daniel Tan
GlassBlade
Published in
6 min readJun 10, 2020

This is my personal notes on monads, that some might find helpful. I’ve included more information in the appendix if needed.

In my experience it is often more useful to talk about what a specific design pattern provides and how it provides it than to talk about what it is, but if you want to play around with the exact definition the monad design pattern has been explained to death on the internet. This is my way of understanding the design pattern by breaking it down into simple steps.

What does it provide, and how?

The monad design pattern allows us to provides a common, easier-to-use API for some context wrapping construct. This is usually implemented through a class or an interface to make it more readable but the notes here use functions for clarity, which will be simple to convert to methods during actual use.

Ruby example with the Optional monad from https://www.youtube.com/watch?v=J1jYlPtkrqQ

Simple steps

The monad design pattern is as follows:

1. Define some type constructor M where a new type M a can be constructed with any type of a. More information at Appendix [a].
Example: Vector or List of type T where T can be anything but this could also be some class or data type.

2. Define a unit function where it creates a new value with type M a from value with type a.

Example:

3. Define a join function that transforms a nested monadic value M (M a) into a value of type M a. More information in appendix[c].

Example:

4. Define a fmap function that takes a function, which takes a value of type a and transform it into a value of type b , but does not change the wrapping context M. I.e. mapping over a list of strings into a list of ints, where the wrapping context remains a list.

Basically here you need to provide some way of interacting with the “inner” value of type a. This usually is the part where it confuses people when dealing with types that are not iterable sequences such as the Maybe type.

Example: Since the chosen types are iterable sequences, this is simply the usual map function.

5. Define a bind function that takes in a monadic value M a and a callback function that operates on type a to create a new monadic value.

This bind function is also basically fmap then join , but you can have intermediate steps depending on the type M a.

Example:

So why monads and how does it help you?

First of all, it improves readability in languages without using meta-programming support (compared to lisp macros, for example) when there are high levels of nesting and repeated code using the bind function. A good example of this can be found in the Wikipedia page for the Maybe Monad and the kotlin documentation for monads.

Secondly, monads offer multiple ways of sequencing two expressions, since you now can also do conditional callbacks in your bind function, allowing you to use intermediate results from the chain. This helps greatly with handling exceptions and empty values.

But why not monads?

Most non-FP languages did not use monads as an explicit design pattern because they didn’t need to.

Imperative languages don’t have this issue of sequencing functions, because they can control when and where to store the results of their function call. This comes with other costs of course.

OOP languages are similar to imperative languages, besides having the Factory and Service Callback pattern.

However, monads are slowly being accepted as another design pattern to consider in non-FP languages, even as unlawful monads (see appendix [d]). For example, Promise in Javascript and Optional in Java are unlawful monads, but they implement the interface for monads regardless. This is because monads allows them to specify a common API for classes that might require a lot of boilerplate code to use, with relatively straightforward steps.

Special mention to lispy programming languages such as Clojure where instead of using an explicit monad pattern, macros allow the user to restructure their code instead of using monads for the two benefits above. For example, the cond macro and threading macros like some-> achieve similar results as using a monad.

Special thanks to Gabriel Lebec for his help on clarifying concepts and extra details that I’ve also included in the appendix.

I try to update weekly, but I was segwayed by the post on “The Pain Points of Haskell”.

Appendix

[a]

The type constructor M is assumed to follow the Functor design pattern, where it wraps some value in a context, and there is some function fmap that can be applied to the value in the context.

[b]

Restrictions for python join and flatten.

It is possible that the outermost Iterable an inner Iterable might be different types, which is outside of the definition of a monad.

Also — the way we expect flatten to work in the context of a monad is that every element of the list will itself be a list — i.e., you won’t have a mix of list and non-list values.

[c]

For a type constructor to be “classified” as monadic, it has to have the capabilities of functors (map) and applicative functors (unit and ap), plus the "new" bit specific to monads (join, or equivalently bind or kleisli arrow). In terms of implementation, it's possible to write the function bind before you write the function map , but until you write the map function, it's just not "complete" as a monad.

I don’t think there are any types for which you can write a correctly-typed bind but for which you cannot write a corresponding map. If there were such types, I don't think they could rightly be called monadic.

Intuitively, this is because it’s “harder” to write bind than map. Also, if you can write bind itself, you can "recover" / define map as bind with unit.

This is part of why you hear people define monad in terms of bind and unit — together they are powerful enough to define functor map and applicative ap . So you can start at the "end" (monad) and then back-fill the first two steps in the hierarchy.

[d]

True monads need to obey the monadic laws. However, it can still be used without it, but the user has to be careful of some presumptions about the monad.

Clojure example using functions we’ve defined above:

Python example:

--

--