This series of articles would be basically my personal study reviews. I would present an overview about monad, and hopefully make it less dreadful as its name looks like for new haskell or functional programming learners.
I assume readers to have a basic grasp of haskell syntax and a liiiittle experience in programming in functional style, but no math background would be required.
Let’s get started
Monad
is maybe the most important programming pattern in haskell, the name “Monad” came from mathematics, but it (maybe) doesn’t really matter that a programmer doesn’t understand what its exact mathematical definition is, so do many other names we came across in haskell.
Motivation
Monad exists everywhere in haskell, we met it when we typed the first (two) line of haskell program
main :: IO ()
main = putStrLn "Hello World"
Usually textbook would say we use IO a
whenever we want to interact with the outside world. But that’s not the whole story, IO itself is an instance of the mysterious Monad typeclass. Haskell add the syntactic sugar with monad called the “do” notation, with which we would be able to write program like
main :: IO ()
main = do
putStrLn "what's your name?"
name <- readLn
putStrLn ("hello " ++ name)
We would delve into this syntax later in this article.
Review of typeclass
Type class is a feature of haskell type system which provides a way to describe a class (set) of types which have similar operations. The simplest case is the Num
typeclass:
class Num a where
(+) :: a -> a -> a
(-) :: a -> a -> a
(*) :: a -> a -> a
-- other "methods"
It basically says: if some type a
has the following operations (+
, -
, *
) defined, then we can treat it as a Num
(number), and we would be able to write more general function using the Num
typeclass instead of a specific type which represents some kind of number (Integer
, Double
etc.). And not surprisingly, Integer
, Double
types are instances of the Num
type class and define their arithmetic operations.
Recall that Monad
in haskell is actually a typeclass, which says that: if some type have some Monad like operations (and follow certain rules, which cannot be enforced by syntax though), then we can call it a monad. And that’s actually the case, we have many monad instances defined in haskell basic library.
the Monad definition
So let’s look at the definition of monad typeclass.
class Applicative m => Monad (m :: * -> *) where
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a
fail :: String -> m a
{-# MINIMAL (>>=) #-}
Leaving the Applicative
aside, which would be explained in later series, the {-# MINIMAL (>>=) #-}
is a pragma that tells the compiler an instance of Monad should define at least (>>=)
and other methods work well automatically (thanks to the default method implementation of Monad). So in no doubt this (>>=)
(pronounced as bind) operation is the core of Monad.
Sadly the type signature of (>>=)
hardly makes sense for new comers. We leave the insight for now. Actually, the magic do
notation we saw in early examples is basically a syntactic sugar of nested (>>=)
operations, let’s see how it works.
Interact with do
Recall our simple example above:
main :: IO ()
main = do
name <- readLn
putStrLn ("hello " ++ name)
We said previously that IO
is an instance of Monad, and readLn and putStrLn produces IO
, here we have:
readLn :: Read a => IO a
putStrLn :: String -> IO ()
and here the type parameter a
for read should (obviously) be String
. And what we got is:
readLn :: IO String
putStrLn :: String -> IO ()
main :: IO ()
Oops, something familiar pops out. If we replace the m
by IO
in the signature of (>>=)
(>>=) :: IO a -> (a -> IO b) -> IO b
According to the type, it’s rather easy to rewrite the program without the do notation
main :: IO ()
main = readLn >>= \name -> putStrLn ("hello " ++ name)
And this representation using (>>=)
is equivalent to the code using do
notation. Although I only give an IO
example here, the do
notation works for every other instances of monad as well, like Maybe
, List
and so on, like:
addIfPresent :: Maybe Int -> Maybe Int -> Maybe Int
addIfPresent mx my = do
x <- mx
y <- my
return (x + y)
which is expanded to
addIfPresent mx my = mx >>= \x -> my >>= \y -> return (x + y)
We would give more simple examples in the next post. And now we introduce the general cases of desugaring do
notation.
-- do notation
do
v <- m
expr
-- where "expr" is the same type of monad as "m", may contains v
-- will be expanded to
m >>= \v -> expr
-- if there are more than two expression
do
v1 <- m1
v2 <- m2
v3 <- m3
expr
-- will be expanded to
m1 >>= \v1 ->
m2 >>= \v2 ->
m3 >>= \v3 -> expr
-- what if no value is extracted?
do
expr1
expr2
expr3
-- could be expanded to
expr1 >>= \_ ->
expr2 >>= \_ -> expr3
-- or simply, recall the method (>>) of Monad
expr1 >> expr2 >> expr3
There are other cases like using let
or pattern matching within do
, those syntax are much simpler to understand.
the Monad chapter of Real World Haskell has a more detailed introduction about do
notation.
Conclusion
Now what we know about (>>=)
(or Monad) is that the only purpose of monad is for enabling the do
notation and greatly improve the readability and simplicity of functional programs. And we can use an imperative programming style while still using pure functional language like haskell. (with certain monad we can even make local variable mutable!).
In this article we introduce an imprecise, simplified view of Monad, how it relates to the ordinary do
notation. And in the following series I will first go through some more simple examples of Monad in haskell, try to give an insight of how monad is introduced, and some more not-so-simple topics.