Monads and why do they matter
This article aims to explain the purpose of monads in functional programming. Yeah, I know, there are tons of information on this subject in various blogs and books, but almost all of them are either too complex with involving category theory and boring formulas explanation or too superficial with describing just main concepts.
So, this is my attempt to show that monads are not just mathematical constructs from category theory, but powerful abstractions to solve real-life problems.
Introducing Monads
We should start with a point that Monad is a design pattern in functional programming. Just like Singleton, Factory, Observer and other well-known patterns from OOP it solves some problem. To get a better understanding what kind of problems it solves, let’s consider a chronology that leads us to monads using.
1. In functional languages, we operate via functions. Let’s define two functions: f
and g
.
2. Now, to write a program we want to execute these functions in specific order. To do it we can compose functions.
3. Function f
might fail (i.e. f(0)
).
Thrown exception is a side effect that can lead to unexpected behavior of the program and should be explicitly handled in all of the places that an exception might occur. This is not the way how we do it in functional languages. So, how do we solve it?
4. Let’s create a new type of data to be returned. A container that represents a computation that may either return a successfully computed value or error. Hence, we can have:
5. But function g
takes a Double
and is not ready to consume a Try[Double]
.
6. Solution: a special function flatMap
to compose sequence of Tries.
We just described monad Try
(*). It encapsulates an effect — computation that may either result in an exception or return a successfully computed value. This is what Monad does — wraps a value and gives it some effect. That’s it. You just take a value, put it into a monad and get back an amplified value. An “amplified” means that the power of value’s type is increased.
There are other kinds of effects. Option
is a monad that gives to wrapped type an effect denoting whether a value is missing. List
is another monad — represents a computation which may return an arbitrary number of results. Future
— gives you a free asynchronous computation of wrapped value.
(*) Actually, Try
fails the left identity monad law. Since this article is mostly for beginners, let’s leave this moment.
Monad functions
Each monad has two functions:
- unit
- flatMap
Scala doesn’t provide a Monad
type like other functional languages does, but, as we mentioned, Monad is not a specific class, it is a concept and we can easily define own trait with functions unit
and flatMap
.
The unit
stands for a single argument constructor of a monad. You give it a value of type A, and it generates you a Monad[A] in return. We define unit
out of the trait because there is no sense to invoke it from already existing monad instance (e.g. tryF.unit(1/x)
). We want it as a standalone static method (e.g. Try.unit(1/x)
).
The flatMap
does the flatting/composing part. It knows how to handle a function which itself returns a value of the monad type. Let’s consider an example.
You could already use List
in Scala collections. Let’s say we have a list of strings:
Now we want to split every world in the list to chars:
Clearly this returns a list of lists: List[List[Char]]
. Sometimes this is what we want, but very often we actually want to get a single flatten list so that, for example, we can iterate over all of the elements. This pattern turned out to be so common that the act of mapping and then flattening is considered to be a basic operation of Monad.
So, what the flatMap
does is that it takes a function with signature String → List[Char]
and flatten our List[List[Char]]
into List[Char]
.
Scala’s for-comprehension (syntax sugar over map
, flatMap
, filter
functions) well illustrates how monads can be chained together in specific order.
Monad laws
There are three laws of monads, namely the left identity, right identity, and associativity. So, every type that has functions unit
and flatMap
and claims to be a monad must obey the laws. If not — it won’t behave like a monad and might do some unexpected things.
1. Left identity
2. Right identity
3. Associativity
The two identity laws basically say that unit
function doesn’t change the value in a monad.
Associativity law says that when we have a chain of flatMap
functions it shouldn’t matter how they’re nested.
Conclusion
Monad is a simple and powerful design pattern for function composition that helps us to solve very common IT problems such as input/output, exception handling, parsing, concurrency and other. Application becomes less error prone. Code becomes reusable and more readable. And Haskell freaks may even notice you around.