Monads Are Just Fancy Semicolons
Monads can be scary — treating them like fancy semicolons makes them fun!
Monads are programmable semicolons. That’s it. For a programmer, a monad provides functions that allow for sequencing actions. Moreover, between every two following actions, a specific code snippet is executed.
So, a monad is a semicolon — but one whose exact behavior you can configure.
Let’s Take a Step Back and Start Slow
In imperative languages like C or Java, semicolons are used to express a sequence of operations. The code in front of the semicolon is executed before the code after the semicolon. In languages like Haskell, where more or less everything is just an expression, it’s not apparent at first glance how to sequence two parts of a program.
Nice to know: Monads originate from a mathematical field called “Category Theory.” For using and understanding monads in Haskell it’s not necessary to know the definitions and theorems of this mathematical discipline.
So, if sequencing is easier to do in other languages, is Haskell badly designed?
No, not at all. One could say, that it was intentional that sequencing was more difficult. Haskell was designed to be referentially transparent, which is just a fancy way of saying that each function call can be replaced with its return value.
Imagine you have a function named
doCalculation which takes a number, does some calculation and returns a number. In Haskell, the expression
doCalculation(x) + doCalculation(x) can be replaced with
2 * doCalculation(x) and we can be sure that the behavior of the program won’t change. In Java, in general this replacement can’t be done in general, as it’s not assured that
doCalculation returns the same result on every call. For example, the method could just call a random number generator and always return a different result. Also, in languages like Java
doCalculation could include common side-effects like writing to a database or log out to the console.
Referential transparency comes with a lot of advantages. Optimizations are easier for compilers of referential transparent languages. Moreover, parallelism can be automated to some degree. However, all these features come with the cost of more difficult sequencing. But for that matter, we can make use of monads.
Let’s make ourselves a monad
How can such a semicolon be programmed and used? Let’s answer this question with an example: Creating and editing shopping lists. A shopping list gets represented as a list of strings. We can implement functions that allow for adding items, one after another, to a shopping list, as well as removing the first item on a list. Additionally, we want to count the number of times a shopping list has been manipulated (adding and removing items).
To begin with, we have to define a new type:
Counter consists of an integer named
counter and an arbitrary element. For our shopping lists, the element will be a list of strings. The counter will represent the number of manipulations done to that list.
Let’s get to the important part. To program our semicolon, enabling sequencing, we do the following:
This is where all the magic happens. To begin with the more straightforward function,
return just constructs a new
Counter for a given element. It can be used to construct a new
Counter starting with
0 manipulations for a given shopping list.
>>= is the “programmable semicolon”. As with everything in Haskell, it’s just another function. In our example it is of type
Counter a -> (a -> Counter b) -> Counter b (for our concrete shopping lists, a and b will both be lists of strings).
Let’s go slowly.
>>= gets a
Counter with an element (called x in the code). It manipulates the element with a given function, which again returns a
>>= returns a new
Counter, in which the integer representing the number of manipulations is the integer of the given
Counter plus the integer of the
Counter returned by the given function plus
What’s crucial in the definition of our programmable semicolon is that the
Counter is known before the function, given in the second argument, can be evaluated. So, if the
Counter in the first argument (which comes before
>>=) is just an expression resulting in a concrete
Counter, we can be sure that this expression will be evaluated before the function given in the second argument (which comes after
>>=) is called.
For the ability to add items to our shopping lists as well as remove them, we need to define two more functions, which are both one-liners. Moreover, we define our first shopping list — namely the empty shopping list.
At this point, we have everything at hand to create a typical shopping list, one item at a time.
Executing the code leads to the following output:
What’s happening here?
We begin with the empty shopping list, which has no elements in it and has its counter initialized to
0. Our programmable semicolon,
>>=, takes the list resulting from a line and passes it to the function in the next line.
The empty shopping list is essentially handed through the lines and manipulated at each stage. The central point is that we can be sure that, for example, “Bread” is added before “Butter.” As described above, that’s thanks to the definition of our programmable semicolon.
But notice how everything in our (as it’s called) “monadic action” is made up of functions. Each line is an anonymous function (except the first), as well as
>>=. In conclusion, we’ve implemented a way to sequence actions in a purely functional way.
Haskell provides a lot of syntactic sugar, which allows for writing the monadic action above in a way that emphasizes the sequential property more:
The two definitions are completely equivalent. In the end, it’s a matter of taste which one to use. To begin with, though, it’s advisable to use the explicit form, to be reminded of what’s going on behind the scenes.
One more important fact about the counting of manipulation. Once defined in the programmable semicolon, the counting is completely invisible within the monadic action. The magic happens inside our semicolon,
>>=. As the functionality of this function can be freely defined, it’s a great place to hide all sorts of things. The predefined maybe monad, for example, hides checking for
null in its programmable semicolon and skips all following lines if
null occurs. Moreover, logging can be implemented in a monadic way and hidden in
>>=. Parsers are another application, which makes use of the state monad — but that’s a whole other story.
So, monads are useful in a variety of scenarios. In the end they’re nothing more and nothing less than programmable semicolons.