UP | HOME

Monads

A monad is defined by:

The >>= operator has the following definition:

(>>=) :: m a -> (a -> m b) -> m b

The return operator has the following definition:

return :: a -> m a

In the above, m is a type constructor that takes one argument. Then, the Monad class definition is:

class Monad m where
  (>>=) :: m a -> (a -> mb) -> mb
  return :: a -> m a

1. do notation

Calls to >>= can be nested:

Just 3 >>= (\x -> Just 4 >>= (\y -> return x+y))

This can be re-written in do-notation as:

foo :: Maybe String
foo = do 
         x <- Just 3
         y <- Just 4
         return x+y

Note that on each line, >>= is being used implictly

2. example: maybe monad

instance Monad Maybe where
  return x = Just x
  Nothing >>= f = Nothing
  Just x >>= f = f x

2.1. how to think about the Maybe monad

What is the point of using monads here? The Maybe provides a context for the computation. The computation, f, that we want to do, only takes place if it's in the right context: namely the Just context.

3. example: list monad

instance Monad [] where
  return x = [x]
  xs >>= f = concat (map f xs)

3.1. how to think about the [] monad

You can think of f as having a non-deterministic output – for a single value, it produces many values. Here, f is also being executed in a non-deterministic context. Note that f doesn't know that it is being executed in a non-determinisitc context. All of that is handled by how >>= is written.

3.2. example: IO monad

An IO monad contains a value that has been obtained via an IO operation. So,

getLine :: IO String
putStrLn :: String -> IO ()

can be combined like:

getLine >>= (\x -> putStrLn x)

Here, x is the 'unpacked' result of the IO getLine, which can be thought of as a box that goes out into the world and fetches a value. Under the hood, the IO monad also introduces data dependencies in such a way that IO operations can be sequenced (see this wiki link).

4. how should we think about monads?

Lots of interesting answers in this stack overflow post.

If we have functions:

f :: String -> Int
g :: Int -> Float

It's pretty obvious how we should compose them using (.). But when we have:

f :: String -> Maybe Int
g :: Int -> Maybe Float

how should we compose them? Monads specify how. It tells us that the composition of these two functions is the following:

\x -> f x >>= g

All the unpacking and re-packing has been handled by the join >>=

We can think of monads as tools for abstraction. Really, the code that we are interested in writing are the functions f. We don't want our program cluttered up by all the code needed to handle composing these functions together. For [], we abstract away the glue code that handles concatenation of the results. For Maybe, we abstract away the glue code that handles termination if Nothing occurs.

4.1. contrast to functors

For a functor, the definition of fmap is:

fmap :: (a -> b) -> f a -> f b

Notice that the function (a -> b) only has access to the value a. It can't specify anything about the output's context. For example, applying fmap to a Maybe will never change a Just a to a Nothing. In contrast, monads allow us to apply functions of the type a -> M b, in which this is possible.

Originally, monads were not a subclass of applicative functors, but they are now.

5. Monad laws

5.1. left associativity

return x >>= f

is equivalent to f x.

I think of return x as the minimal containing box for x. Then, binding f to that box will unpack the value and then apply f. This should be the same thing as just applying f to x.

Or: return is a left-identity with respect to >>=.

5.2. right identity

m >>= return

is the same thing as m. Note that here, m is a single concrete type, such as Just String.

So, unpacking something from a monadic wrapper and then re-packing it in the minimal containing wrapper will result in the same thing as constructing the monad directly.

5.3. associativity

(m >>= f) >>= g

is the same as

m >>= (\x -> f x >>= g)

The first one is read as: apply f to m to get a new monadic value, and then apply g to that new value.

The second one is read as: apply to m, the function that unpacks x, applies f to x to get a new monadic value and then applies g to the new value.

6. Useful functions

6.1. replicateM

Applicative m => Int -> m a -> m [a]

Then, replicateM 5 as is equivalent to:

do a1 <- as
   a2 <- as
   a3 <- as
   a4 <- as
   a5 <- as
   pure [a1,a2,a3,a4,a5]

6.2. sequence

The definition (from here):

sequence :: Monad m => [m a] -> m [a]
sequence = foldr mcons (return [])
  where mcons p q = p >>= \x -> q >>= \y -> return (x:y)

sequence takes a list of monadic computations and returns a list of their results, wrapped in a monad. Note that if using the IO monad, using >>= will cause the IO action to run.

7. useful links

Created: 2024-07-15 Mon 01:26