Many tutorials explain Monads but left me questioning, under what scenario did someone jump up and proclaim “We need to invent monads to solve this problem!”? I’m less interested in Haskell specifically and more interested in the concepts so here’s an attempt at motivating monads with basic secondary school Algebra.
Forget you know how to program for a moment.
When you were a kid, you probably learned about functions in Algebra 1. You probably learned to write out tables for functions like this:
f(x) = x^2 + x + 1
f(0) = 1
f(1) = 3
f(2) = 7
After this, maybe you learned about function composition:
g(x) = 2x
f(g(0)) = 1
f(g(1)) = 7
f(g(2)) = 21
Still with me? Hope so. Let’s add a new function into the mix:
h(x) = √x
Now, I need you to remember back to middle school, before you were taught imaginary numbers existed. Let’s try to write out a table.
h(-1) = UNDEFINED
h(0) = 0
h(1) = 1
h(2) = 1.414…
But you can’t take the square root of negative numbers! Fortunately we can just write down UNDEFINED in our table and be done with it.
But what happens when we compose this function with the others and try to to calculate the following function?
What does it mean to calculate this? What does this calculate to?
I don’t think we can calculate it! How about this — let’s say that it returns UNDEFINED as well. While we’re at it let’s also say this too:
f(UNDEFINED) = UNDEFINED
And for simplicity, let’s say that we can’t calculate the value of a function that has an UNDEFINED input, any function will just return UNDEFINED. The problem with this is two-fold:
- If we had to tell our calculator to calculate these functions, we would have to modify every single function to know to return UNDEFINED for an UNDEFINED input.
- Once we hit an UNDEFINED value, we don’t actually need to calculate the rest and can skip further calculations and just return UNDEFINED. We could end up wasting a lot of time. Our functions are simple, but imagine they looked something like this:
f(x) = 422.55X + X^2 + 44X^1.5 + 66X^-4
How can we write a function that skips the rest of the calculations if we hit UNDEFINED?
Try to think of something yourself, and in the meantime, you can step back out of your middle school self.
OK, BREAK OVER
Next is something that’s super simple, but not normally part of middle school “Math”: a function that can take another function and return a new function. Note: this f is a reference to a function variable, not the f defined above.
p(f) = 2f
Let’s take one of our functions and try it out:
p(g) = 2g = 2(2x) = 4x
And then try calculating a table:
(p(g))(0) = 0
(p(g))(1) = 4
(p(g))(2) = 8
This may not seem all that different from what we have above, and you would be correct! The next part is piece-wise functions. You may have heard of them in middle school.
q(x) = 2x, if x is even x, if x is odd
Hopefully you can calculate the table on your own here.
THE MAGIC TRICK (that’s actually not magic at all)
Okay, back to our earlier question, how can we write an expression that avoids calculating functions with UNDEFINED inputs? Let’s try this
m(x, f) = UNDEFINED, if x is UNDEFINED f(x), if it isn’t
Now, let’s try to calculate this:
m(m(h(x), g), f)
For an invalid value:
m(m(h(-1), g), f)
m(m(UNDEFINED, g), f)
Ta da! We got an UNDEFINED result, with a mathematical definition that even a calculator could understand and we didn’t need to even attempt the later functions!
We just wrote a monad!
A what? A monad. A “Programmable Semicolon.” A means to add functionality by mixing a special function in between each function you want to execute. Sometimes this requires your functions to conform to certain behavior (i.e. receive certain variables) other monads can work without custom adaptation. The monad function we just explored is loosely based on the Maybe Monad, which allows you to chain functions together in such a way so that functions can maybe return a value, or maybe not, in which case you can cancel future calculations.
Let’s try another example. Say we wanted to ensure that we want to execute three functions in a specific order, but they don’t actually depend on one another. How can we write a function which adds an order to inherently unordered set of calculations?
f(x) = x
g(x) = 3x
h(x) = x + 2
Calculate: g(1), f(2), h(3), in this order
These functions are independent from each other, the result is the same, regardless of which order we calculate them. How can we ensure their order? We need to make a dependency! Let’s try this:
m(n, f, v) = n + f(v)
Ok, now let’s sandwich this between our functions
m(m(m(0, g, 1), f, 2), h, 3)
m(m(3, f, 2), h, 3) (we just executed g(1))
m(5, h, 3) (we just executed f(2))
10 (we just executed h(3))
We can ignore the result, it was just a tool. As you can see, by putting our monad function between functions, we controlled the order of calculation! This allows us to use our math functions to imitate other forms of calculation which are ordered by design (read: imperative programming).
This is a variation of the State Monad! It allows us to order events by injecting the state (in our case the sum of all previous steps) into each function.
Back to the real world
Most programming languages have functions which can do more than the functions in this post. For example, they can save variables and return different values when you call each function different times. Haskell is different; Haskell’s functions are the same as the functions here, except they can operate on more than numbers. Both Haskell’s and our functions are known as “pure functions” because they only depend on the input variables to calculate their outputs.
Remember how you might have had math problems like this in algebra?
Simplify the last function using the first two functions:
g(x) = 2x
h(x) = 4 + 2x
f(x) = 2*g(x) + h(x)
f(x) = 2*2x + 4 + 2x
f(x) = 6x + 4
Because these are pure functions, we can simplify this function quite significantly and save a lot of needless calculation. Furthermore, we could calculate a table of the values of the final function once for the inputs we desire and never have to calculate anything again!
Now imagine if we redefined the following
g(x) = a different random number for each calculation
How would you simplify things now? It’s much trickier. You can’t simplify nearly as much as before and you also can’t write a definitive table for inputs and outputs. This is a small window into why functional programming is nice if you can figure out how to make it work.
You can make infinite monads and I’ve only shown you a couple. Hopefully you can now understand why you need monads in certain scenarios instead of just what on earth they are.
Another fun example is thinking about a monad that allows us to calculate bank withdrawals. Since our functions can’t ask for a bank balance within a function, how can we write a monad that passes along a bank balance and stops transactions if we run out of money? Hint: try combining the concepts of the two previous monads into one.
I’m a horrible Haskell programmer, my brain simply has a hard time reading a lot of the syntax and this has gotten in the way of understanding many of the concepts. I would be delighted to be wrong, so if I am, please correct me! Ping me at [email protected] or @newhouseb.
The final example, which finally made monads “click” for me was based on the one given in this great InfoQ talk on Purely Functional I/O here: http://www.infoq.com/presentations/io-functional-side-effects
I would highly recommend you watch it.