More on Monads
Recall from Chapter 4.5 (Railway Pattern#Monads) that monads support composition in context. This idea extends beyond the composition of functions that each branch out to happy and sad paths in the railway pattern. As you have seen, other types like []
don't have much to do with the railway pattern, but is still a monad. This because as long as a type describes some notion of computation, it can be a monad which supports composition in context. We have also seen how this can be useful when the programming language supports easy monadic computations, for example, with Haskell's do
notation.1
However, if you observe the definition of the Monad
type class carefully (see GHC Base: Control.Monad), you might notice that there are more methods and monadic operations than just return
and >>=
.
Ignoring values
In an imperative programming language like Python, we can write standalone expressions as statements, primarily to perform some side-effects. For example:
def my_function(x):
print(x) # standalone statement
return x
We can, in fact, write the print
statement in the style of z <- print x
in Haskell, although that would be useless since that variable's value is not used at all and is not meaningful to begin with:
def my_function(x):
z = print(x) # why?
return x
Therefore, monads also have a method >>
that basically discards the result of a monadic action. This method has the following type signature, which, in comparing with that of >>=
should make this more apparent:
class Applicative m => Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
As you can tell, unlike >>=
, the second argument to >>
is not a function, but is just another term of the monad. It ignores whatever a
is in context in the first argument, and only uses it for sequencing with the second argument of type m b
.
Thus, do
notation actually uses >>
when composing monadic operations when the result of an operation is to be discarded. We give some more rules of do
notation, including the rules for translating let
binds, which allows pure bindings, in contrast with <-
which defines a monadic bind. Note that in do
notation, there is no need to write in
for let
binds:
do s ==> s -- plain
do e1 <- e2 ==> e2 >>= (\e1 -> do s) -- monadic bind
s
do e ==> e >> do s -- monadic bind, ignore
s
do let x = e ==> let x = e in do s -- pure bind
s
For example, we have seen how >>=
on lists performs a for
loop of sorts. For lists, >>
does more or less the same thing, except that the values in the previous list cannot be accessed. For example,
ghci> [1, 2] >>= (\x -> [(x, 3)])
[(1, 3), (2, 3)]
ghci> [1, 2] >>= (\_ -> [3])
[3, 3]
ghci> [1, 2] >> [3]
[3, 3]
Of course, >>
on lists is not particularly useful, but we shall see some uses of >>
for other monads shortly.
Monadic Equivalents of Functions
Due to the prevalence of monads, many of the familiar functions like map
and filter
have monadic equivalents. These are usually written with a postfix M
, such as mapM
or filterM
. In addition, such functions can also ignore results and are written with a postfix _
, such as mapM_
or filterM_
. We show what we mean by "monadic equivalent" by juxtaposing the type signatures of some familiar functions and their monadic counterparts:
map :: (a -> b) -> [a] -> [b]
mapM @[] :: Monad m => (a -> m b) -> [a] -> m [b]
filter :: (a -> Bool) -> [a] -> [a]
filterM :: Monad m => (a -> m Bool) -> [a] -> m [a]
Let us see some examples of mapM
in action:
ghci> map (+2) [1, 2, 3]
[3, 4, 5]
ghci> map (Just . (+2)) [1, 2, 3]
[Just 3, Just 4, Just 5]
ghci> mapM (Just . (+2)) [1, 2, 3]
Just [3, 4, 5]
One example of mapM
over lists and Maybe
s is with validation. Let us suppose we want to read a list of strings as a list of integers. To start with, we can use a function readMaybe
that attempts to parse a String
into a desired data type:
ghci> import Text.Read
ghci> :{
ghci| toInt :: String -> Maybe Int
ghci| toInt = readMaybe
ghci| :}
ghci> toInt "123"
Just 123
ghci> toInt "hello"
Nothing
The mapM
function allows us to ensure that all elements of a list of strings can be converted into Int
s!
ghci> mapM toInt ["1", "2", "3"]
Just [1, 2, 3]
ghci> mapM toInt ["hello", "1", "2"]
Nothing
Monadic Controls
Another useful tool that comes with monads are control functions. For example, in an imperative program we might write something like the following:
def f(x):
if x > 10:
print(x)
return x
In Haskell, since if
-else
statements are actually expressions and must have an else
branch, we might have to write something like the following:
f x = do
if x > 10
then someAction x
else return () -- basically does nothing
return x
Notice the return ()
expression. Because every "statement" in a do
block must be monadic, we must write a monadic expression in every branch. In addition, we are clearly using someAction
for its monadic effects, so the "returned" value is completely useless, likely just ()
(the unit type, which means nothing significant). Therefore, the corresponding else
branch must also evaluate to m ()
for whatever monad m
we are working with. This is a chore and much less readable!
Instead, we can use regular functions to simulate if ... then ...
statements in a monadic expression. This is the when
function defined in Control.Monad
2:
when :: Applicative f => Bool -> f () -> f ()
As you can tell, when
receives a boolean condition and one monadic action and gives you a monadic action. Importantly, the monad wraps around ()
, which means that this operation is useful for some monadic effect, such as IO
. This allows our function above to be written as:
import Control.Monad
f x = do
when (x > 10) (someAction x)
return x
Although later we will see that the monadic action someAction
can actually cause side effects, it is not necessarily the case that side effects are the only reason why a monadic action m ()
is useful. Another example of this is the guard
function:
guard :: Alternative f :: Bool -> f ()
If the monad you are working with is also an Alternative
, the guard
function, essentially, places a guard (like guards in imperative programming) based on a condition, returning the sad path immediately if the condition fails. To see this in action, let us see how we can use guard
to implement safeDiv
:
import Control.Monad
safeDiv1 :: Int -> Int -> Maybe Int
safeDiv1 x y = if y == 0
then Nothing
else Just (x `div` y)
safeDiv2 :: Int -> Int -> Maybe Int
safeDiv2 x y
= do guard (y /= 0)
return $ x `div` y
An Alternative
is an applicative structure that has an empty
case. For example, an empty
list is []
, and an empty
Maybe
is Nothing
. The definition of guard
makes this really simple:
guard :: Alterative f => Bool -> f ()
guard True = pure ()
guard False = empty
Notice how guard
works in safeDiv2
. If y
is not 0
, then guard (y /= 0)
evaluates to Just ()
. Sequencing Just ()
with return $ x `div` y
gives Just (x `div` y)
. However, if y
is equal to 0
, then guard (y /= 0)
evaluates to Nothing
. We know that Nothing >>= f
for any f
will always give Nothing
, so Nothing >> x
will also always give Nothing
. Therefore, Nothing >> return (x `div` y)
will give us Nothing
. As you can see, guard
makes monadic control easy!
As before, guard
works on any Alternative
. For this reason, let us see how guard
works in the []
monad:
ghci> import Control.Monad
ghci> ls = [-2, -1, 0, 1, 2]
ghci> :{
ghci> ls2 = do x <- ls
ghci| guard (x > 0)
ghci| return x
ghci| :}
ghci> ls2
[1, 2]
As you can see, guard
essentially places a filter on the elements of the list! This is because [()] >> ls
just gives ls
, whatever ls
is, and [] >> ls
just gives []
. In fact, >>
over lists somewhat like the following function using a for
loop in Python:
>>> def myfunction(ls2, ls):
... x = []
... for _ in ls2:
... x.extend(ls)
... return x
>>> my_function([()], [1, 2, 3])
[1, 2, 3]
>>> my_function([], [1, 2, 3])
[]
As you can tell, if f
is False
, then guard f >> ls
will give []
; otherwise, it will just give ls
itself. This makes it such that we now have a way to filter elements of a list! Better still, if we combined this with something else:
ghci> import Control.Monad
ghci> ls = [-2, -1, 0, 1, 2]
ghci> :{
ghci> ls2 = do x <- ls
ghci| guard (x > 0)
ghci| return $ x * 2
ghci| :}
ghci> ls2
[2, 4]
Notice how we have just recovered list comprehension! The definition of ls2
can also be written as the following:
ghci> ls = [-2, -1, 0, 1, 2]
ghci> ls2 = [x * 2 | x <- ls, x > 0]
ghci> ls2
[2, 4]
Thus, as you can see, list comprehensions are just monadic binds and guards specialized to lists! Even better, do
notation allows you to use guards
, monadic binds etc. in any order and over any monad, giving you maximum control over how you write monadic programs.
Other languages like Scala also have similar facilities for writing monadic computations. In fact, the Lean 4 programming language takes Haskell's do
notation much further (Ullrich and de Moura; 2022).
The monadic control functions described in this section are defined in the Control.Monad
module in Haskell's base
library, i.e., they need to be imported, but do not need to be installed (just like the math
library in Python).
References
Sebastian Ullrich and Leonardo de Moura. 2022. do
Unchained: Embracing Local Imperativity in a Purely Functional Language (Functional Pearl). Proceedings of the ACM on Programming Languages (PACMPL). 6(ICFP) Article 109 (August 2022), 28 pages. URL: https://doi.org/10.1145/3547640.