Updated

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 Maybes 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 Ints!

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.Monad2:

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.


1

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).

2

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.