Posted on

I keep being confused about what a monad is. I have previously convinced myself that I understand, but the feeling of understanding doesn't seem to last very long. So I thought I'd just write it down here in a way that is easy for me to understand should I get confused again in the future.

Monads are a mathematical thing defined in terms of operations and properties that need to hold. Haskell and some other languages have typesystems advanced enough to specify a monad as construct in the language. While other languages don't necessarily have sufficiently advanced typesystems to be able to represent a monad as an interface you can implement, any combination of a type and functions that satisfy the right properties is still a monad.

In Haskell the Monad typeclass looks like this:

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

If you're like me this looks like absolute gibberish, so let's decode it a little bit. >>=, >> and return are functions. >>= is called bind and >> is sometimes called then. >> is just a shortcut for >>= with a value instead of a function for when m b doesn't depend on a.

With that sorted out, let's transform this typeclass definition into a pseudo syntax that a curly-bracket-brained person like me can read more easily:

class Monad m {
  fn bind(m<a>, fn(a) -> m<b>) -> m<b>;
  fn then(m<a>, m<b>) -> m<b>;
  fn return(a) -> m<a>;
}

There are also rules associated with this interface: https://wiki.haskell.org/index.php?title=Monad_laws.

Let's translate these to our pseudo-syntax as well:

Left identity: bind(return(a), h) == h(a)
Right identity: bind(m, return) == m
Associativity: bind(bind(m, g), h) == bind(m, fn(x) -> bind(g(x), h))

Monadic I/O

A while ago I made a very simple Rust implementation of an I/O monad to help me understand how monads are helpful for purely functional I/O. This isn't practically usable because bind accepts a function pointer and so you can't use closures there. And closures are an important part of making monadic I/O actually work of course.

enum IO<T> {
    GetChar(Box<dyn FnOnce(char) -> IO<T>>),
    PutChar(char, Box<IO<T>>),
    Done(T),
}

impl<A: 'static> IO<A> {
    fn bind<B: 'static>(self, f: fn(A) -> IO<B>) -> IO<B> {
        match self {
            IO::GetChar(g) => IO::GetChar(Box::new(move |c| g(c).bind(f))),
            IO::PutChar(c, x) => IO::PutChar(c, Box::new(x.bind(f))),
            IO::Done(x) => f(x),
        }
    }

    fn then<B: 'static>(self, b: IO<B>) -> IO<B> {
        match self {
            IO::GetChar(g) => IO::GetChar(Box::new(move |c| g(c).then(b))),
            IO::PutChar(c, x) => IO::PutChar(c, Box::new(x.then(b))),
            IO::Done(_) => b,
        }
    }

    fn ret(a: A) -> Self {
        Self::Done(a)
    }

    fn eval(self) -> A {
        match self {
            IO::GetChar(g) => {
                let c = std::io::stdin().bytes().next().unwrap().unwrap() as char;
                let next = g(c);
                next.eval()
            }
            IO::PutChar(c, x) => {
                print!("{c}");
                std::io::stdout().flush().unwrap();
                x.eval()
            }
            IO::Done(x) => x,
        }
    }
}

fn put_char(c: char) -> IO<()> {
    IO::PutChar(c, Box::new(IO::Done(())))
}

fn get_char() -> IO<char> {
    IO::GetChar(Box::new(|c| IO::Done(c)))
}

This can then be used like this:

get_char()
    .bind(|c| put_char(c))
    .then(put_char('h'))
    .then(get_char())
    .then(get_char())
    .then(IO::ret('r'))
    .bind(|c| put_char(c.to_ascii_uppercase()))
    .eval();