The first time I heard about Monad was at a Scala Meetup. Later, I tried to understand the concept of Monad but was overwhelmed by various voluminous books and tutorials on Haskell. Then I came across Ruan Yifeng’s article “Illustrated Monad“ published in 2015. Although it was clear and easy to understand, it was detached from Haskell, and the illustrations didn’t match the concepts in the language. Ruan Yifeng’s article was translated from “Functors, Applicatives, And Monads In Pictures,” which I read through.
Computer programs are used to control a computer to perform calculations. The objects operated on by programs are various types of values, such as numerical values. Here’s a simple value 2
:
Using a function to process the value can return the result of the function execution, such as:
Apart from simple numerical types, values can also be contained within some contextual environment, forming more complex value types. You can think of the contextual environment as a box, with the numerical value placed inside the box. This box as a whole is described as Just 2
, which is a boxed 2
:
If you are familiar with Java, you can think of this box as a wrapper class, like Integer and int, corresponding to boxed and unboxed 2.
Facing a boxed 2
, we can’t directly apply the +3
function to it:
At this point, we need a function fmap
to operate on it. fmap
will first extract the value 2 from Just 2
, then add 3 to it, put the result 5 back into the box, and return Just 5
:
How does fmap
know how to parse Just
? What if it was another type like Only
, could it still parse it? That’s why Functor is needed to complete the definition of this operation.
A Functor is a type of data type:
Functor defines the behavior of fmap
:
fmap
has two input parameters and one output parameter. The input parameters are a function and a boxed value, and the output parameter is a boxed value. It can be used like this:
fmap (+3) (Just 2)
-- Just 5
Returning to Haskell, the Haskell “system library” has an instance of Functor
called Maybe
, which defines the behavior of fmap
, specifying how to operate on values when the input parameter is of type Just
:
instance Functor Maybe where
fmap func (Just val) = Just (func val)
fmap func Nothing = Nothing
The entire process of the expression fmap (+3) (Just 2)
is similar to this:
Similarly, from the definition of Maybe
, it can be seen that if the second parameter passed to fmap
is Nothing
, the function will return Nothing
. This is indeed the case:
fmap (+3) Nothing
-- Nothing
Now suppose a scenario in Java where a user uses a utility class Request to make a request to a server, and the request returns a type Response. Response is an entity class that may or may not contain the required data:
Response res = Request.get(url);
if (res.get("data") != null) {
return res.data;
} else {
return null;
}
In Haskell, using fmap
would become:
fmap (get("data")) (Response res)
Of course, Haskell doesn’t have a get("data")
syntax. You can encapsulate the operation of getting Response.data
as a function getData
, then pass it into fmap
as the first parameter.
Haskell provides a syntax sugar <$>
for fmap
to simplify its usage:
getData <$> (Response res)
Next, think about how Haskell functions operate on lists. The function performs calculations on each element of the list and then returns a list:
In fact, lists are also Functors. Here is the definition of lists:
instance Functor [] where
fmap = map
Applicatives
is another concept. We previously said that values are placed in boxes. What if functions are also placed in boxes?
Haskell provides the operator <*>
to handle functions inside boxes:
For example:
Just (+3) <*> Just 2 == Just 5
Using <*>
can also accomplish some interesting operations, such as multiplying and adding each element in a list:
[(*2), (+3)] <*> [1, 2, 3]
-- [2, 4, 6, 4, 5, 6]
Function execution involves processing values using functions with arguments, involving three roles. Functors
place the processed value in a box, Applicatives
place the function in a box, and Monads
place the function’s argument in a box. Monads have an operator >>=
to achieve Monad functionality. Suppose there is a function half
that takes a numerical value as an argument. If the value is even, it divides by 2; otherwise, it returns Nothing:
half x = if even x
then Just (x `div` 2)
else Nothing
How do you pass a Just
type value to half
?
>>=
can solve this problem:
Just 3 >>= half
-- Nothing
The >>=
operator takes Just 3
, transforms it into 3
, and processes it in half
. A Monad
is a data type that defines the behavior of >>=
:
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
Here, Maybe
is a Monad
(existing alongside the Maybe
mentioned above):
instance Monad Maybe where
Nothing >>= func = Nothing
Just val >>= func = func val
>>=
also supports chain operations:
Just 20 >>= half >>= half >>= half
-- Nothing
Although Haskell’s Monad is quite famous, it actually involves three concepts: Functors
, Applicatives
, and Monads
. Perhaps Monad has more extensive applications. In data processing, FP is not superior to OOP; the logic is similar, only the writing style differs. Facing the same problem with different thinking and expressions corresponds to different programming ideas and paradigms. There are many intricate theories in the world waiting for us to explore.