July 11, 2023

Understanding Monads - 2023

This is not another monad tutorial. It is my own list of important things to know, when you are thinking about Monads.

First think about "map"

If you have a basic understanding of functional programming, you understand map. You have a function and a list and map produces a new list with the function applied to every member of the original list. Easy.

Generalize this to fmap

Rather than a list, consider some general sort of "container" with something "in it". What fmap does is to apply a function to such a containerized thing and return the result of the function in the same container. For example the container could be a Maybe. We can apply fmap to the Maybe because Maybe is a functor. A functor is simply a type-class that defines the fmap function. Don't let these fancy names like "functor" scare you. This is pretty basic and simple. There is an infix operator for fmap that is (<$>). If you understand map, understanding fmap just involves generalizing the idea of a list as a container to some other (any other) kind of container.

Monads in their most basic form

A type that is a Monad (i.e. implements the Monad type class) is obliged to define the following two functions.
class Monad m where
    (>>=)  :: m a -> (a -> m b) -> m b
    return :: a -> m a
Another function (>>) is also defined for a Monad but for now it is worth just considering these two.

"Return" is famous, mostly because the name is so misleading. It is nothing whatsoever like return in any other language. People have suggested that "inject" might be better. What is does is simple, it takes its argument and makes it a monad of whatever type we are dealing with.

The other function (>>=) is often called "bind". Understanding it strikes at the heart of what Monads are all about, or how they can be useful (or used at the very least). Keep reading.

To understand (>>=) and (>>) consider this:

class  Monad m  where
    (>>=)  :: m a -> (a -> m b) -> m b
    (>>)   :: m a -> m b -> m b
    return :: a -> m a
And consider this definition.
m >> k  =  m >>= \_ -> k
Leaving aside (>>) for now, let's look a "bind". The first argument is "m a" (something in a container). The second argument is "(a -> m b)" which is a function that takes "a", does something to it, and puts it in a container.

So bind could be thought of like this. We have something in a container. Bind removes it, applies the function to it, and rewraps it. Simple as that.

So what about (>>). It is just a handy "convenience" function. You use it when you are binding once monadic computation to another, and the second one does not require the result of the first one.
Here is the legal definition:

    (>>) :: m a -> m b -> m b
    m >> k = m >>= (\_ -> k)

What about do notation

You will see this explained in all kinds of ways that don't really give full understanding. What "do" notation is, is a special mini-language (a DSL, i.e. domain specific language) for dealing with monads. In particular, it hides the use of (>>=) and (>>) internally.
Feedback? Questions? Drop me a line!

Tom's Computer Info / tom@mmto.org