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>andOption<T>.
If you don’t know Result and Option, you can
- read about them in my German lecture notes,
- find them splattered across the Rust book, or
- watch this interesting video where the
Optiontype is calledMaybe.
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.
- Identity from the left
is fulfilled, since
return(v).bind(f) == f(v)whereSome(v).and_then(f) == f(v)f: impl FnOnce(T) -> Option<U>. - Identity from the right
is fulfilled, since
m.bind(return) == mm.and_then(Some) == m - Associativity
is fulfilled, since
m.bind(f).bind(g) == m.bind(|x| f(x).bind(g))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.