If you have grasped the mathematical basics, and if you have understood what’s a monoid, you are ready to discover monads. Of course, we’ll continue to have fun with F#. There are tons of approaches trying to explain what monads are. Personally, I found out once I understood the problem they are solving, thus it will be the approach of this blog post. Again, the following code can be found online.
First compositions rule
Remember the function composition rule: “The result of each function is passed as the argument of the next, and the result of the last one is the result of the whole “.
It means that functions must be “pluggable” (the output of the first must be compatible with the input of the second). But It also implies that we need a side effects free world. If one of the function can throw an error, there is no more composition because the result of my function (an exception is thrown) is not passed as the argument of the next, and the result of the last function will not be the result of the whole. Instead it breaks the runtime, and someone at a higher level (the caller) must deal with it.
Note that there is nothing to explicit the fact that isPositive function can fail in this code. It compiles as everything seems fine, but it can fail at runtime.
Sometimes vs Always
What if instead of thowing an error sometimes (if i >0), we always return a Result, which is either a success containing the value, or a failure?
I can wrap my result into something to defer the side effects. Of course it implies changing my signatures. My functions are no longer compatible, because a Result<int> cannot be used as the input by the isPositive function, it expects a simple int.
Bind to the rescue
What if I write a simple transformation function, that I can call bind. Its roles are:
- In case of success: to wrap my simple int into a Result<int>, and to call the next function with the wrapped result
- In case of failure: to return a failure, and not to call the next function, because after all, we don’t need to execute the next function if the previous one fails.
And now I can call this function between two incompatible functions and… Voilà! They are pluggable again!
My final composition will look like this, and this time it compiles.
Of course, you probably don’t want to create special functions to apply the bind. F# like many languages allow us to define a proper operator bind: >>= (it is the common sign for bind operator).
A more elegant composition will look like this:
I get rid of side effects, thus I can compose my functions.
Dude, you lost me.
No problem, take a breath and think about that:
- I wanted to compose functions, but they have side effects
- I wanted to get rid of side effects, thus I introduce the Result<T> instead of throwing an exception
- My functions where no longer “pluggable”, thus I introduced a bind function to transform a simple type into a more complex Result<T> type.
- We defined a new operator bind (>>=) to replace the explicit bind call
- In the end I can have a nice composition, without the side effects
This can be resume by: we find a way to compose function, despite the side effects.
And where’s the monad?
Let’s decrypt the Wikipedia definition of a monad and see if something match:
“Monads allow developer to build pipelines that process data in a series of steps, in which each action is decorated with the additional processing rules provided by the monad.”
Sounds familiar? In our case we had a pipeline (ToInt -> IsPositive -> ToString), and we decorated each step with the ability to convert from T to Result<T> (and to escape the pipeline in case of failure). You understand that we could have done other things in our bind function, such as Logging, Exception handling, IO management… We managed side effects in the bind used to compose the functions, instead of in the functions themselves.
“A monad is defined by a return operator that creates values, and a bind operator used to link the actions in the pipeline; this definition must follow a set of axioms called monad laws, which are needed for the composition of actions in the pipeline to work properly. The result of the last one is the result of the whole.”
I think we talked enough about the bind function. In our case, the return function is as simple as: return x -> x
Here are the monad’s laws: return acts approximately as a neutral element of bind, in that:
- (return x) >>= f ≡ f x
- m >>= return ≡ m
Binding two functions in succession is the same as binding one function that can be determined from them (in other words: bind is associative):
- (m >>= f) >>= g ≡ m >>= (x -> (f x >>= g))
This is what these tests demonstrate:
Thus, the type we defined Result<T>, with the associated bind and return functions is a monad. The container alone is not a monad. The bind function alone is not a monad. But the container Result<T> with the bind function, because this function defines the identity function (i.e. a return function) when used with a neutral element, and because it is associative, is a monad.
Let’s finish the Wikipedia decryption:
“Purely functional programs can use monads to structure procedures that include sequenced operations like those found in structured programming. Many common programming concepts can be described in terms of a monad structure without losing the beneficial property of referential transparency, including side effects such as input/output, variable assignment, exception handling, parsing, nondeterminism, concurrency, continuations, or domain-specific languages. This allows these concepts to be defined in a purely functional manner, without major extensions to the language’s semantics.”
And because input/output, variable assignment, exception handling, parsing, nondeterminism, concurrency, continuations and DSL are very common IT problems, FP use monads a lot to manage them. I hope it is now clear for you how we used a monad to structure our chain of operation, and how we managed exception in a purely functional manner.
If so, you now understand what’s a monad, and why it is so useful, especially in FP. If not, please let me know and I’ll try to improve my explanations!
References: If you feel frustrated and want to go further, I recommand you the excellent Category Theory for Beginners, and the even better (but harder) Category Theory for Programmers.