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
Option
type 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 Option
s
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 match
ing going on. Fortunately, Option
comes with the and_then
method.
This can be used to compose both Option
s and Result
s.
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 Option
s, 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) == m
m.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 Option
s and Result
s 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_then
s. 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., Option
s 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.