Monads: Your App as a Function, Part 2
This is part 2 of Monads: Your App As A Function.
In the previous post we started looking at monads, and summarized that their core purpose is to transport values or computations along a call chain. (Functional programming is all about inputs and outputs.) To enable this behavior, we said monads have the following properties:
- Monads are types with two functions
- Monads are constructed via unit to contain arbitrary data
- Monads are chained together (potentially transforming the contained data) via flatMap
We concluded the article by saying that this is not the entire truth. What we have neglected so far are the laws that govern unit and flatMap, and that have to hold true for a type that looks like a monad, to actually be a monad.
Let me begin by saying that there is no deeper meaning in these laws beyond supporting common sense. I still think it’s worth looking at them because they close the loop to imperative programming, which you might be more familiar with, since especially the second and third laws enable what’s been attributed to monads as being “programmable semicolons” in case you’ve stumbled upon that phrase before.
The three monad laws
As I mentioned, it’s not actually sufficient to merely provide the unit and flatMap functions in order to write a monad type. These two functions must also behave in a certain way. This behavior is summarized in three short algebraic laws, and they are as follows.
The law of associativity says exactly what you’d expect it to. It demands that
for any monad
m and any two functions
it must not make a difference whether you apply f to m first and then apply g to
the result, or if you first apply g to the result of f (recall that functions
passed to flatMap return monads) and then in turn flatMap this over m. Or
What does that mean or why is it important? It simply means that the order in which
you compose individual steps must not affect the overall outcome. This doesn’t
have any deeper meaning beyond not violating common sense: if you pour yourself
a coffee, it shouldn’t make any difference whether you first pour the coffee,
then add sugar and cream, or first mix sugar and cream and add coffee to it.
(I deliberately chose coffee, not tea, since any true Englishmen would disagree
with this statement!) To clear up with a common misunderstanding: the law of
associativity says nothing about the order of execution; only about the order
of composition. Since the computation defined by
f often involves side effects,
the order of execution obviously does matter (writing a file to disk and then
formatting the hard drive has a different outcome than formatting the hard drive
and then writing files to it!)
Left and right identity
Now these two are more interesting. The left unit law (“left identity”) states that:
Or in prose: flatMap must behave in such a way that for any function
f passed to it,
the result is the same as calling
f in isolation. This might be a bit
more difficult to grasp at first, but beyond the aspect of chainability that
we’ve already discovered, this law enables the “3rd C”: confinement.
Essentially it says: flatMap allows you to peek at the value contained
in the monad and apply a transformation to it, all without leaving the monad.
This is also why monads are called “programmable semicolons”: they allow you
to “lift” an imperative statement into the confinement of the monad using a
higher order function (flatMap) and chain it to the next computation, rather than
separating the two computations by calling them explicitly and placing a
semicolon between them (The “semicolon” here is not meant to be understood literally,
but as a metaphor for demarcating expressions in an imperative call style.
Even in languages that do not use semicolons, this applies.)
That is, given two monad types
M2, instead of saying:
1 2 3 4
You can say:
That is, we have obtained the result without explicitly peeking at
by replacing imperative calls with a more fluent functional call style. An important
take away here is: this leaves no room for further side effects in flatMap.
Since the outcome must be equivalent as per this law, flatMap must not “squeeze”
any extra side effects between the “semicolons”.
That leaves only the right unit law (“right identity”). Let’s have a look at its formal definition:
Or in plain English, applying the unit constructor to a monad has the exact same outcome as not calling it at all. Again this makes perfect sense if you translate it to an imperative programming style. Instead of saying:
You can say:
How does that make sense? It turns out that this law is important to support
a fluent call style, since it allows us to build up long monad call chains
without worrying that a function passed to flatMap might represent the value
we’ve already obtained. This becomes more obvious if in the code snippet above
you replace the call to
Monad.unit with some arbitrary
f, which might return
the unit constructor. Again, this law states that there is no room for side
effects here. For a nice example of why this law is important, I suggest having
a look at Scala’s
Option[T] monad, or Guava’s
Optional<T> if you prefer Java.
Now that you know the laws, forget about them
You saw this coming, right? I honestly think that the monad laws mostly exist to prevent behavior that would be counter-intuitive. In fact, there is at least one very good case where the left unit law should be violated: exception handling. To recall, the law states that:
But what if
f throws an exception? If
flatMap is supposed to behave the
same way as the right hand side, then it will, too, throw an exception and
terminate the call chain. That’s bad, since it destroys one of the most valuable
aspects of monads: chaining computations together. In defense of the purists,
in mathematics, there is no such thing as exceptions. Signals don’t magically
stop functions and make them return nothing. Unfortunately, in the real world
we’re faced with functions that are not pure in a mathematical sense, so instead
we take the “third C”, confinement, a little further and transform the exception to a value
and trap it in the monad. This is exactly what RxJava does when trapping an
exception and re-routing it to Observer#onError, or
the Try type in Scala. So while their type structure is monadic, they actually
violate the left identity law and hence are not true monads. But who cares as
long as they get the job done!
Speaking about getting the job done: we’re now able to rewrite that piece of code we kicked things off with in the first article, and transform it to a fluent code style using RxJava’s monad(-ish) Observable type.
Tying up the ends
Now that you understand what monads are and why they’re useful, let’s put the knowledge to practice and revisit our initial code snippet. Here it is again for your convenience:
1 2 3 4 5 6 7 8 9 10
Remember how I said that we can think of each line of code here as a step in a series of transformations, and that the monad helps us transport the results from one step to the next. We also said that each step may terminate the entire task by throwing an exception. Just to emphasize: this is bad. It means the entire task is not really deterministic, and what we’re missing are well-defined “exit points”, that allow us to terminate the sequence gracefully. It all reminds us of a really fragile soap bubble, where on each line we risk the bubble to burst, without having a good exit strategy.
We can rewrite this using a monadic type now, in this case an RxJava
which lets us turn the soap bubble into
something less fragile:
1 2 3 4 5
I’ve used Java 8 closure syntax here to keep the example concise and clear.
Note how we transformed our semicolons into a monadic call chain.
does not immediately return a value anymore; instead, it returns an
holding the API call result. We then want to perform a side effect by caching
this result, which we do using another monad transformation called
an action that’s performed for every value emitted.
We then transform the fetch result into another monad, namely from
Observable<JsonObject> by passing a function to
flatMap that parses
the JSON String and sticks it in a new monad/observable. We finally subscribe
a listener that receives the final result.
There’s a few key things here that make this implementation superior to what we started out with, and I suggest to compare these against our initial Q&A we went through in the previous article:
RxJava ensures that if in any of the above steps an exception is thrown, it will be propagated to the observer and subsequent steps will be skipped. In other words, we don’t have to worry about errors until we actually need to deal with them.
You can gracefully terminate the call chain yourself by calling
onCompletedon the given observer. This allows you to skip any subsequent steps in case there is nothing meaningful to return, i.e. there’s no need to return
nullanywhere. The observer will simply receive nothing through
onNextand complete straight away.
In RxJava, scheduling an individual step to run on another thread than the one you started on, is treated as just another transformation of the call chain. This means you can use a monad transformation to specify concurrency, making it a simple and natural aspect to deal with.
Most importantly, all of the above applies to all possible steps in the call chain, freeing you from the burden of making decisions for every single step in your sequence.
Instead of a soap bubble, we have an assembly line now, where results of individual steps are transported in a resilient and well defined way. Exit points are clear and dealt with uniformly, regardless of where we leave the monad–even in the case of error.