Monads
04 Jun 2020Monads have been on my mind for about a week now. I got to them when I was going through a bit of Haskell code and reading through the documentation for the methods side by side (how it came to be is a story for another day). Anyway, this is not about Haskell since Monads are just a design pattern that’s used in functional programming.
I found them very intimidating at the first glance after I foolishly looked at the types1 of monads and the functions working on them. So, this post is mainly meant for me to straighten up my thoughts about monads by trying to present them in a friendly manner.
Motivation
I’d like to motivate the use of monads. Before we see what they actually are, it’s better to see the problem they solve.
While there are multiple use cases of monads let me motivate their use through just one of them, the Exception[T]
Monad2.
Suppose we want the functions to be able to indicate through their result if any exception occurred inside the function. Suppose we define a type Exception[T]
with two constructors:
Failure[T]: String -> Exception[T]
Success[T]: T -> Exception[T]
For example, a function could return Failure[Int]("Division by zero")
if there was a division by zero exception, or Success(10)
if there was no exception and 10
was the result of the computation.
So, how would we string together functions returning values of type Exception[T]
? For example, we could have a div(a, b)
function that calculates a/b
amd its implementation would be:
def div(a: Int, b: Int): Exception[Int] =
if (b == 0) Failure("Division by zero")
else Success(a / b)
Let’s also have a modulo function similarly3:
def mod(a: Int, b: Int): Exception[Int] =
if (b == 0) Failure("Division by zero")
else Success (a % b)
Now, how would we calculate (a / b) % c
with this? Here’s an approach that uses pattern matching (if you’re unfamiliar, please refer to the previous post):
def func(a: Int, b: Int, c: Int): Exception[Int] =
div(a / b) match {
case Failure(s) => Failure(s)
case Success(num) => mod(num, c) match {
case Failure(s) => Failure(s)
case Success(num) => num
}
}
That does the job, but man was it a mouthful to write. All that boilerplate just for a simple operation. It would get much more complicated if we were to add a few more operations. Here’s where monads come into picture. They are just a way to remove all the boilerplate, and it results in much more readable code. We’ll look at how the use of monads makes this code much more easier to read later in the post.
What Monads do
With the motivation, out of the way let’s see what monads do.
Let me take a step back, and look at function composition. Functions f: a -> b
and g: b -> c
can be composed as gof: a -> c
which is defined as gof(x) = g(f(x))
This composability of functions make them extremely useful since we can divide our logic into more manageable pieces with this.
It’s similar with monads. Suppose M[T]
is the monad type, then the functions returning the monads would look like f: a -> M[b]
and g: b -> M[c]
.
Notice we can’t compose these functions as we did earlier since we can’t feed a value of type M[b]
to g
which expects a value of type b
.
So, each monad defines a composition operator (commonly called bind) which takes a monad, extracts the underlying value and feeds it into the other function.
I’ll denote this operator by >=>
4 and its type is M[a] * (a -> b) -> M[b]
5. To the left of >=>
is a monad of type M[a]
and to the right is a function which takes in a
value of type a
and returns a value of type b
. >=>
takes these parameters and returns a monad of type M[b]
.
Improvements using Monads
The previous section defining the bind operator was a bit abstract since there are a lot many types of monads. Here let’s focus on the Exception[T]
monad and see how we can define the bind operator for it.
Notice that whenever a function returns a failure, we want to propagate that, but when it returns a success, we want to feed the underlying value to the next function. Using this intuition, the bind operator would look like:
def >=>[T, U](m: Exception[T], f: T -> Exception[U]): Exception[U] =
m match {
case Failure(s) => Failure(s)
case Success(value) => f(value)
}
That didn’t look so bad, and now look how we can write the previous mess succintly:
def func(a: Int, b: Int, c: Int): Exception[Int] =
div(a / b) >=> (num => mod(num, c))
Note that num => mod(num, c)
is a function that takes in a value called num
and returns mod(num, c)
. Such expressions are called anonymous functions or lambda functions.
Observe how this was arguably more simple than the previous example, and definitely more readable. With the bind operator, we can string together many such functions and all the exception handling is taken care of for us.
There is a further syntactic sugar for this, I’ll use Scala’s for
expressions here, but note that Haskell also has a very similar do
notation. With for
expressions, the above function would look like
def func(a: Int, b: Int, c: Int): Exception[Int] =
for {
num <- div(a, b)
ans <- mod(num, c)
} yield ans
This a lot more cleaner and note that it’s exactly the same as the previous expression.
See also
This was just a very basic intro regarding the intuition behind monads, there’s a lot more to it. For that refer to these:
- Brian Beckman: Don’t fear the Monad: It’s a great lecture regarding the intuition I tried to cover here. The What Monads do section has been primarily influenced by the talk.
- Monad - Wikipedia: As always, Wikipedia also has a great article on Monads, though it is a bit technical.
Regarding the syntax used
The code written here is primarily pseudocode and is definitely a weird jumble of Haskell and Scala, with some bits from here, some bits from there, and even a fusion of these at some places. I like how this has turned out, but perhaps it could be confusing due to the mixture of syntaxes. Please let me know of any issues or possible improvements regarding that.
Footnotes
-
By types, I mean the type system used in statically typed programming languages ↩
-
T
is a type that represents a type that can be anything, also called a generic type. The syntax is similar to Scala, though the concept is the same in Java and C++. ↩ -
Arguably, we can remove the code repetition here by the use of higher order functions, but that’s not the point of this post. ↩
-
The notation is used by Haskell, instead of the Scala notations I have been following, and I find it quite nice ↩
-
Unlike Haskell, I have defined this without currying. I find it makes it easier to understand, though the curried version can be simply made from this function too ↩