It’s Time You Learn About Monads
Monads, with a name that originated in metaphysics and its roots in pure mathematics, is a concept that might seem esoteric at first. The “aha!” moment that builds our intuition for it is a Satori that many developers trying to understand functional programming want to reach, finally consummating in a blog post such as this one. In the book What I Wish I Knew When Learning Haskell, Stephen Diehl suggest an Eightfold Path, of which the first two are:
- Don’t read the monad tutorials.
- No really, don’t read the monad tutorials.
There’s also a known “monad tutorial fallacy,” which basically states that there are no shortcuts. You can’t transfer intuition with metaphors of burritos or magical boxes. If you want to understand Monads you’ll have to study the details and build the intuition yourself. But as you’ll see, Monads are not as complex or esoteric as they sound.
Some History: The Ivory Tower
Languages such as C, Fortran, and Pascal started up “bottom up” from Turing Machine, registers, and adders, and built up abstraction after abstraction towards “for loops” and Classes. Academic languages such as Lisp, ALGOL, SmallTalk, and later Haskell trickled their way downwards, subtracting abstractions from Lambda Calculus and Category Theory to reach the metal, often resulting in “ivory tower” languages. The former camp values performance, is pragmatic, and currently dominates industry, the later values formal verification and “equational reasoning,” which lets us reason about our code in provable axiomatic statements, and not just pseudo-code.
Objects and Classes, for instance, were integrated in high-level, highly principled languages such as Simula and SmallTalk, and introduced to low-level C to create Type abstractions for representing elements in the code, first as macros, later in their own languages such as C++ and Objective-C. C itself is an integration of flowchart theorems, called structured programming, coming from academia, and were first implemented in ALGOL. Structured programming introduced the abstractions of if statements, for loops, subroutines (functions), etc.
The use of Monads in programming originated from a LICS paper by Eugenio Moggi, were he used them to abstract computations (such as state and exceptions) independent of specific computational models. Philip Wadler recognized that this technique could be applied to structure functional programs. Eventually it found it’s way into Haskell and other functional languages such as F#, and Scala, etc. Haskell implemented many of its constructs around it, such as I/O, which we’ll touch later on.
You don’t need to understand them to use them well
You don’t need, however, to understand Monads to use them. John von Neumann wrote to a friend:
“Young man, in mathematics you don’t understand things. You just get used to them.”
Haskell provides many built-in Monads, which you’ll encounter in “hello world” tutorials (Lists, Maybe, IO), and their syntax can be sugared to look like imperative programming with “do” notation.
See this example of Monadic function composition with the ‘bind’ operator, which defines a putFile function which reads a filename from the user and print its contents:
putFile = getLine >>= readFile >>= putStrLn
This is a function pipeline handling the
IOMonad, which implemented the bind operator
>>= . This syntax is called “point-free,” it reasons about functions holistically, without their variables. It can be sugared with ‘do’ as:
putFile = do
filename <- getLine
contents <- readFile filename
In the ‘do’ notation, the arrow
<- unwraps the monad
IO String (IO of type String) into filename of type
String, and does the same with readFile, which takes filename as a parameter.
As a counter to von Neumann’s quote above, Albert Einstein is attributed saying:
“Any fool can know. The point is to understand.”
- A cake is a hot cake that has been cooled on a damp tea towel, where a hot cake is a prepared cake that has been baked in a preheated oven for 30 minutes.
- A preheated oven is an oven that has been heated to 175 degrees C.
- A prepared cake is batter that has been poured into prepared pans, where batter is mixture that has chopped walnuts stirred in. Where mixture is butter, white sugar and brown sugar that has been creamed in a large bowl until light and fluffy…
As you can see, the effort lies in describing “what things are” and not so much on “how to” make them. The cake “is” the immutable result of a pipeline of functions. Half jokingly he later adds:
“Maybe a version using monads wouldn’t be as confusing.”
This can be understood in two ways: Either the monadic code is expanded into
do notation, which would regain the imperative feel of the original recipe, or it is composed as a point-free pipeline with binds.
Recipe with do notation:
bake ingredients = do
mixture <- creamInLargeBowl ingredients
batter <- mixWithChoppedWalnuts
preparedCake <- pourIntoPreparedPans batter
hotCake <- bakeInPreheatedOven preparedCake
cake <- coolOnDampTeaTowel hotCake
Recipe with point-free binds:
bake = creamInLargeBowl >>= mixWithChoppedWalnuts >>= pourIntoPreparedPans >>= bakeInPreheatedOven >>= coolOnDampTeaTowel
Let’s Get to the Details
To create a Monad, we simply have to provide the following:
- a type, let’s say
M. A data-transfer object, often a simple Tuple of values. This type can also be a function which returns the data object. It’s Haskell’s convention to separate data and the methods as two separate definitions. These conventions mirror mathematical syntax.
- a type constructor, called the
returnfunction in Haskell, or “unit” in math. Wraps any type
a). One should see
M<a>as the type of a computation that returns a value of type
a. In most languages
returnis a reserved word, part of the language syntax, which causes confusion when beginners see it invoked as function in
doblocks. It’s a function which wraps a value. The type converter introduces a simple value into the monad world. There’s no function to do the opposite (*safely), which is where
bindis used instead. We’ll implement this converter with the name
unit. Note that the unwrapped value could be a function as well.
- a type combinator, or
bindfunction. A function which unwraps any type
aand returns the type
M<b>. This is done in Haskell by providing the user-defined ‘bind’ operator
>>=, which like all operators in Haskell, is an infix function. It’s a higher-order function which takes a monad
M<a>and a user-provided function
(a) => M<b>and returns an
M<b>. In case the value doesn’t need unwrapping, a simpler
>>can be provided — sometimes called
then— which takes two monads,
M<b>and returns type
M<b>. This allows chaining monads in the way we chain functions with
;in C-like languages. It’s like
bindbut ignores the
aparameter, that is, the result of the first monad while binding to the second. We’ll implement it as
Alternatively, in theory, monadic function composition can be achieved with a
join function which unwraps
M<a> . Providing either is mathematically equivalent, see Wikipedia’s definition for proof.
We can also provide relevant methods to our monad class. And, because we are expecting things to fail, Haskell provides a default definition for a
fail function, sometimes called
catch, which can be overridden. The fail function will be called upon encountering an error in unwrapping during bind. This is of course not part of the mathematical definition of a Monad. Equivalent to using try-catch.
Furthermore, monads also implement the
Functor interface. This interface could be called a “mappable,” that is, it implements the “fmap” function, which being a container of type
M<a>, takes an
a -> b function and returns a container of type
M<b> . Haskell didn’t originally require monads to implement
fmap , so it’s a point of confusion. Applying primitive functions in wrapped objects, such as a functor or a monad is called “lifting” a function. Since it’s useful we’re going to implement it as well.
- an Array of strings is a functor
- its higher-order
mapfunction can take, for instance, a
string -> numbercallback function, and returns an Array of numbers,
In Haskell, “List” is a Monad, and
map is implemented with
fmap, the functor interface.
Let’s type it out
Let’s define the interfaces for Functors and Monads. I have to give a generic type for both the monad and functor, and for the unwrapped type.
This won’t compile however. This is because in TypeScript I can’t write “higher-kinded types” (a generic of a generic), i.e.:
Monad<m<a>> , so I have to define all type permutations in the method definitions. I used
ma instead of
m<a>, which became a bit verbose. Hopefully dynamic typing will help. (It’s a known issue when trying to represent monads in TypeScript).
I’m using a classes and interfaces to define Monads, just like Haskell — a purely functional language — does. Haskell is a typed functional language, unlike predecessors like ML. Also I’m using generics to emulate Haskell’s Type Constructors. I chose lower-case
b instead of conventional
Now let’s define the trivial Monad, called the “Identity Monad.” I had to do some ugly casting, as TypeScript forced me to provide intermediate cast with
unknown. This is not production code, just didactic, so bear with me:
In the Identity monad, bind is just simple function application.
Let’s use it (also trivially):
In TypeScript we don’t have operator overload, neither we have infix functions. So
a bind b has to be written as
bind(a, b). We can sugar the syntax a little bit with indentation:
The bind can continue with other wrapped types as well. This means that if we end the bind chain with it, it would define the returning type of the entire chain:
Theory: Monad Laws
Monads require adherence to certain formal laws by definition. These laws are not checked by the Haskell compiler, and are the scope of Category Theory. They should be obeyed, or the use of the Monad would turn counter-intuitive.
We’re used already to formal laws in our code. Such as our if statements following De Morgan’s laws. Otherwise the language would rightfully appear broken.
Wrapping a value and binding it to a function is equivalent to calling the function on the value.
id.bind(id.unit("foo"), (str) => id.unit(str.toUpperCase()))
is equivalent to:
Binding a monad to a wrapper function is equivalent to the monad itself. The bind essentially unwraps the monad, and the wrapper function wraps it again.
id.bind(id.unit("foo"), (str) => id.unit(str))
is equivalent to:
Associativity is the property in which rearranging parentheses does not change the result. With Monads, the parentheses around bind operations can be rearranged without affecting the result. When dealing with higher-order functions the parentheses might not be so obvious at first. Here, because bind function is prefix instead of infix, its location changes, so there’s no need for parentheses.
(helloStr: string) => id.unit(helloStr + " World"))),
(helloWorldStr: string) => id.unit(helloWorldStr.toUpperCase())
is equivalent to:
(helloStr: string) => id.bind(
id.unit(helloStr + " World"),
(helloWorldStr: string) => id.unit(helloWorldStr.toUpperCase())));
Binding a functor to wrapper function composed together with a function is equivalent to mapping the function to the functor.
id.bind(xs, (x) => id.unit(f(x)));
is equivalent to:
Example: The State Monad
Notice the binding logic in lines 15 and 16. This is what makes bindings “magic,” as it’s the hidden gears which during the use of the monad. This time I chose the Module syntax instead of defining a class. Either way, there’s no mutable state in them.
How Do Monads Do I/O?
In Haskell, all I/O functions receive and return the IO Monad.
getChar, for instance, is of type
putChar of type
IO<void> . This means that operations on those functions are in a monadic continuum. This, however, does not mean that Monads can do I/O, only that in Haskell, I/O functions provided by the compiler have been encapsulated into the language as Monads. This also encapsulates the do-notation’s imperative style so it doesn’t taint the entirety of the code.
Taking our previous example of sugared monadic code using do-notation:
But the imperative style can’t leave the do-block, so the imperative style doesn’t pollute other parts of the code. The researchers Simon Peyton Jones and Philip Wadler, dubbed this “imperative functional programming.” And because of the Monadic properties described above, equational reasoning is not affected.
The similarity is also shallow, since each assignment in the do notation is a sugared bind function. The ‘de-sugared’ code might actually unfold like this:
Monads in this case act as a barrier of the imperative sub-language to the rest of the purely functional language. An experienced programmer could rewrite Haskell’s do-notation as a point-free function pipeline, it’s still after all:
How Does Haskell Itself Perform Real I/O With Monads?
Let me make this point clear: Haskell’s language doesn’t do side-effects. It’s immutable and equational, pure mathematical transformations. However, Haskell’s compiler and compiled code do!
Monads are not a way to “cheat” away the purity of the language. The result of all function calls are determined by their arguments. Also they don’t “get around” immutability and side effects, which includes all the changes in the real world such as databases, filesystem, console printing, network data, etc. All function calls can be replaced by the result of a previous call with the same parameters.
type IO<a> = (w: World) => [a, World];
// the type IO of type 'a' is a function which given a World,
// will return a value of type 'a' together with a World.
An I/O computation is a function that takes the state of the world, and returns a modified world as well as the return value. Compilers might be powerful but can’t create a copy of the world, so they mutate the world, yet the language itself is kept formally pure, pass-by-copy.
A simple function which changes the world might look like this:
Values get passed between functions by copy, mutated or not, in strict order. This is not expensive in Haskell, since once the compile realizes that we return null, it omits all unnecessary intermediate values.
The key idea of using Monads for I/O was to tread all I/O actions as modular “computations,” which, when performed, actuates input and output before delivering a value of type
Of course, the compiler does not actually pass the world around. It passes a dummy token to ensure proper sequencing of actions, since lazy evaluation could be compute them out of order, when needed. The compiler then performs input and output as actual side effects.
Things That You Can Do With Monads
- A state transformer: used to thread state through a program. A state transformer is a function that takes the old state (of type
s) and returns a value (of type
a) and the new state (of type
- A state reader: It accepts a state that the computation may depend upon, but the computation never changes the state.
- An exception monad: either returns a value or raises an exception.
- A maybe monad: models a computation which may fail to yield a value at any point during computation.
- A continuation monad: computations which can be interrupted and resumed.
- A list monad: can be used to model non-deterministic computations, which return a sequence of values.
- A writer monad: which lets us emit a lazy stream of values from within a monadic context.
- A parser monad: can be used to model parsers. The input is the string to be parsed, and the result is list of possible parses, each consisting of the value parsed and the remaining unparsed string. It can be viewed as a combination of the state transformer monad (where the state is the string being parsed) and the list monad (to return each possible parse in turn).
- Random numbers monad: given a seed, produce a random number.
- Concurrency: extends the IO monad to fork threads, each performing I/O, and communicating with each other.
- Software Transactional Memory (STM): related to concurrency composition and its problem of writing to synchronized, mutable locations. Deep.
- Quantum Computations: a great example on how you can create an embedded domain-specific language with Monads. As they are a flexible mechanism for combining operations in a way that reflects the semantics of the intended domain.
- Be creative!
All these applications are explicit about their effects, and their composition serves as a way to define and structure them.
How Do I Get Out Of The Monad? a.k.a How Do I Debug-Print?
There is no safe way to escape from the IO Monad. Therefore, it has do be done unsafely. Haskell provides
unsafePerformIO, the “back door” into the
IO monad, which unwraps the
IO<a> into an
This means that we can take away all the context of the Monad and use the wrapped values instead of binding.
Unsafe functions can break type safety, interfere with lazy IO, or break parametricity.
Immutable data structures have been implemented in Immutable.js and Immer, utility functions and currying are implemented in Ramda, funfix, and Lodash/fp. React components can be written functionally with Hooks. But no Monads in sight.
A framework could help override the binary right-shift assignment operator
>>= to work with point-free function notation, or providing infix functions. It could also implement template and utility functions found in Control.Monad library, such as
sequence, conditional monadic operations, and function “lifting.”
Conclusion: Should I Use Monads?
If you’re using a pure functional style in your code, then Monads can abstract most of your function binding, turning a lot noise into “magic.” It modularizes logical units of computation, and facilitates refactoring: Adding one parameter to a function chain will involve one change in the Monad definition, instead of the entire function chain.
If you’re not using a pure functional style, then I hope this was educational. De-mystifying Monads can help you understand the benefits and perks of functional programming. You could try implementing your next algorithm functionally. And if this was interesting, you can delve further into theory. Computer Science is a new field and we’re still trying to figure out how to do things well.