Monads, as they occur in functional programming, seem crazy and difficult to understand to me. Above all explanations and references to the mathematical subfield of category theory are confusing to me. In this post, I’m trying to explain a concept that I haven’t penetrated very deeply myself, in an introductory, application-oriented and superficial way, so that you can become the head nerd at the next party of imperative nerds.

This is a translation of the original German article in a collaboration between man and machine.

Examples in Rust

Monads M<T> are types that encapsulate inner values of a type T and provide additional functionality. Examples of monads in Rust are

  • Result<T, E> and
  • Option<T>.

If you don’t know Result and Option, you can

The Result type is made for error handling. In addition to the result of a calculation in case of success, Result contains an error of type E in case of failure. Similarly, Option models the existens or absence of a value.

We can use

let xr = Ok(5.0);

or

let xo = Some(5.0);

to encapsulate corresponding inner values in Result and Option.

Composition of Options

First, let’s look at an example application. We want to build a safe calculator for integers. The user passes a comma-separated string of two numbers and a character that is either +, -, *, or /. The parsing of the characters and the number-strings may fail. Also the calculations may fail in case of an overflow or division by 0. For the sake of simplicity, we use Option and not Result to handle errors. The value None signals the occurrence of an error. The parse functions have the following signatures.

// Only +, -, *, and / are valid arguments.
fn parse_operation(
    op: char
) -> Option<fn((u16, u16)) -> Option<u16>>;

// Needs a comma in the string.
fn split(s: &str) -> Option<(&str, &str)>;

// Arguments need to be number-strings.
fn parse_operands(
    (n, m): (&str, &str)
) -> Option<(u32, u32)>;

Our safe calculator can be implemented as follows.

fn safe_calculate(numbers_str: &str, op: char) -> Option<u32> {
    let op = parse_operation(op);
    match op {
        Some(op) => match split(numbers_str) {
            Some(number_strs) => match parse_operands(number_strs) {
                Some(numbers) => op(numbers),
                None => None,
            },
            None => None,
        },
        None => None,
    }
}

There is a lot of manual matching going on. Fortunately, Option comes with the and_then method. This can be used to compose both Options and Results. The Rust method and_then of the enumeration type Option has the following signature.

pub fn and_then<U, F>(self, f: F) -> Option<U>
where
    F: FnOnce(T) -> Option<U>

Our calculation function results in its much more compact form.

fn calculate(numbers_str: &str, op: char) -> Option<u32> {
    let op = parse_operation(op);
    op.and_then(|op| 
        split(numbers_str)
        .and_then(parse_operands)
        .and_then(op))
}

The method map of Option with the following signature is also a tool for composition.

pub fn map<U, F>(self, f: F) -> Option<U>
where
    F: FnOnce(T) -> U

However, map is not sufficient if f can fail, since the type of its result would be something like U = Option<V>. So we would end up with an Option<Option<V>>. To unpack nested Options, there is the method flatten. Hence, the method and_then corresponds to the use of map followed by flatten. This is why and_then is often called flat_map in other languages.

Monad Definition

In functional programming languages, the name bind is often used instead of and_then or flat_map. The wrapping of a value v of type T in an Option<T> by Some(v) corresponds in functional languages to a function, which is often called return or unit. A more detailed explanation of bind and return can be found in the OCaml-Tutorial*OCaml is a functional programming language.. In a functional language*We use pseudocode in the following, which Rust programmers hopefully can read well. we obtain for all monads M with inner type T thus by let m = M::return(v) an instance m of M<T>. The instance m is mapped by let n = m.bind(f) with the help of a function f: T -> M<U> to the instance n of M<U>. Analogously to and_then in Rust, the signature of bind corresponds to the following snippet.

bind: (M<T>, T -> M<U>) -> M<U>

Note that bind maps monads of the same outer type M to one another. Thereby, the type of the inner value may change. Besides the existence of bind- and return-analogies for Option and Result, the following rules are also fulfilled. This allows Option and Result to be called monads.

  1. Identity from the left
    return(v).bind(f) == f(v)
    
    is fulfilled, since
    Some(v).and_then(f) == f(v)
    
    where f: impl FnOnce(T) -> Option<U>.
  2. Identity from the right
    m.bind(return) == m
    
    is fulfilled, since
    m.and_then(Some) == m
    
  3. Associativity
    m.bind(f).bind(g) == m.bind(|x| f(x).bind(g))
    
    is fulfilled, since
    m.and_then(f).and_then(g) == m.and_then(|v| f(v).and_then(g))
    

In contrast to Option and Result, implementations of the trait Iterator are strictly speaking not monads. They lack a bind-method. The closest thing to a bind is flat_map. But flat_map maps each instance of the iterator iter::Filter to a iter::FlatMap. From bind, however, we expect that the same monad falls out at the end that was put in. Nevertheless, Iterator behaves somewhat monadic.

Why all this?

A core property of functional languages is the separation of side effects from pure functions. Examples of side effects are

  • state changes of local variables at least in the local scope,
  • the output of text to a screen, and
  • the deletion of files.

Pure functions are functions that depend deterministically on their arguments and do not change states outside their scope.

Advantages of monads are:

  • Monads are a tool for separating side effects from pure functions.
  • Thereby, the existence of effects will be made visible by the types, namely the monads.
  • Furthermore it is a monadic intention to enable or simplify modular program structure.
  • Languages like Haskell that support monads natively provide useful functions that can be applied to any monad.

Besides Options and Results there exist other examples of monads. We discuss two more examples in the following. Those are especially useful to separate effects.

A State Monad

In the purely functional language Haskell, for example, there is a state monad that makes it possible to write large parts of the program in pure functions. Using bind these can be concatenated for state manipulation. In Rust, a simple state monad could look like this.

#[derive(Clone)]
struct StateMonad<T> {
    state: T,
}
impl<T> StateMonad<T> {
    pub fn new(state: T) -> Self {
        StateMonad { state }
    }
    pub fn and_then<G>(
        self, 
        f: impl FnOnce(T) -> StateMonad<G>
    ) -> StateMonad<G> {
        f(self.state)
    }
}

With the state monads we can, for example, implement the Builder pattern.

struct House {
    has_garden: bool,
    has_basement: bool,
    area: f64,
    n_rooms: u8,
    description: String,
}

impl House {
    fn new() -> Self {
        House {
            has_garden: false,
            has_basement: false,
            area: 80.0,
            n_rooms: 4,
            description: "A beautiful house".to_string(),
        }
    }
}

fn add_basement(h: House) -> StateMonad<House> {
    StateMonad::new(House {
        has_basement: true,
        ..h
    })
}
fn add_garden(h: House) -> StateMonad<House> {
    StateMonad::new(House {
        has_garden: true,
        ..h
    })
}
fn set_description(h: House, description: String) -> StateMonad<House> {
    StateMonad::new(House { description, ..h })
}

fn build_a_house_with_garden(description: String) -> House {
    StateMonad::new(House::new())
        .and_then(add_basement)
        .and_then(add_garden)
        .and_then(|h: House| set_description(h, description))
        .state
}

An IO Monad

In Haskell there is an input/output monad which among other things ensures that text appears on the screen. All functions in Haskell that have an input/output side effect such as displaying text on the screen are marked accordingly. Their signature contains the IO monad. This means that functions without an IO monad in their signature are free of corresponding side effects. The functions that we pass to bind or and_then in Haskell are functions that do not manipulate IO states. The IO monad encapsulates the return value of the side effect as its inner value. The following is a simplified implementation in Rust.

struct IoMonad<F, T>
where
    F: FnOnce() -> io::Result<T>,
{
    effect: io::Result<F>,
}
impl<F, T> IoMonad<F, T>
where
    F: FnOnce() -> io::Result<T>,
{
    pub fn from_err(error: io::Error) -> Self {
        Self { effect: Err(error) }
    }
    pub fn new(effect: F) -> Self {
        IoMonad { effect: Ok(effect) }
    }
    pub fn and_then<G, U>(
        self,
        f: impl FnOnce(T) -> IoMonad<G, U>
    ) -> IoMonad<G, U>
    where
        G: FnOnce() -> io::Result<U>,
    {
        match self.apply() {
            Ok(x) => f(x),
            Err(e) => IoMonad::from_err(e),
        }
    }
    pub fn apply(self) -> io::Result<T> {
        self.effect.and_then(|effect| effect())
    }
}

The IO monad could now be used to isolate all IO effects from the rest of the program. However, in Rust we cannot rely on the fact that no effects are executed outside a monad. We can chain different IO monad operations as in the case of the state monad.

fn house_str(h: House) -> String {
    format!(
        "{} {} {} {}\n{}",
        h.has_garden, h.has_basement, h.area, h.n_rooms, h.description
    )
}
fn main() -> Result<(), io::Error> {
    let file_path = "src/main.rs".to_string();
    let read_desc = IoMonad::new(|| std::fs::read_to_string(&file_path));
    read_desc
        .and_then(|desc| {
            let h = build_a_house_with_garden(desc);
            let h_str = house_str(h);
            IoMonad::new(move || io::stdout().write_all(h_str.as_bytes()))
        })
        .apply()?;
}

Read and write operations happen within the and_thens. Functions such as house_str or set_description are of pure kind.

A Monad Trait

The methods new and and_then are used by all monad examples. A monad trait could be helpful, whose face could correspond to the following snippet.

pub trait Monad<T> {
    fn new(self, x: T) -> Self;
    fn and_then(self, f: impl FnOnce(T) -> Self<U>) -> Self<U>;
}

However, there is no Self<U> in Rust. This is a so-called higher kinded type, which Rust does not yet support. An attempt at a solution can be found in this article* I am not judging. I have not dealt with the article in detail .

Do I need to know this as a Rust user?

No. Understanding what a monad is is not necessary to effectively program in Rust. It is sufficient and significantly more important to understand the functionality of specific types like Option or Result or the Builder pattern, even if the general concept of monads is not known or internalized. By the way, this aligns the strategy of the purely functional language Elm. It allows programmers to work with monads without explicitly declaring them.

Since Rust does not natively support monads, there are no helper functions for monads. In Haskell, for example, there is a function sequence that takes an instance of a traversable type L<M<T>> with monadic elements of type M<T> as arguments. The result is the swapping of M and L. Let’s say I have a list of Maybe monads, i.e., Options in Rust. The application of sequence yields a Maybe monad encapsulating the list. In Rust, similar behavior is implemented in the collect method of iterators. By using

[Some(1), Some(2), Some(3)].iter().collect::<Option<Vec<_>>>()

we get

Some([1, 2, 3])

In Rust, many monadic concepts are implemented for specific types or traits, even though the concept of monads does not exist. The specific types like iterators do even not have to be monads.

On the other hand familiarity with monads can be useful for Rust programmers.

  • A look beyond one’s own plate helps to broaden the horizon.
  • Rust supports many aspects of functional programming. You might want to orient yourself further towards functional programming.
  • Especially functional languages often support monads. Code examples from other languages can be helpful and inspiring.
  • To assess whether a language feature is important or not, knowing it is helpful. It also doesn’t hurt to be familiar with some design patterns of object-oriented programming, even if you don’t intend to use them.
  • My personal main reason for delving into monads is simply curiosity.