Skip to content

What a monad is, and why Rust never says the word

Level: 301 · deep dive

One line: A monad is a wrapper type plus a way to chain operations that also return that wrapper, without the layers piling up — you have been using three of them since your first Option, and Rust's decision to use monads without calling them monads is one of its better ones.

The problem it names

Start with map, which you know. Map a plain function over an Option and you get an Option back. Fine.

Now map a function that can itself fail:

fn half(n: i32) -> Option<i32> {
    if n % 2 == 0 { Some(n / 2) } else { None }
}

let nested = Some(8).map(half);   // Option<Option<i32>>

Two layers, and nothing wants two layers. Chain three such calls and you have Option<Option<Option<i32>>>, which is unusable — and the nesting has no meaning, because "maybe maybe a number" is just "maybe a number".

and_then is map followed by flattening the result:

let flat = Some(8).and_then(half);   // Option<i32>

That is the whole idea. A monad is any type with those two things: a way to wrap a plain value (Some, Ok, vec![x]), and an and_then that chains wrap-returning functions without nesting. Everything else written about monads is consequences of that.

Verified output of what_a_monad_is.rs — regenerated by tools/run_examples.py, never hand-typed.

1. `map` NESTS when the function itself can fail
   Some(8).map(half)       = Some(Some(4))      <- two layers, unusable

2. `and_then` is the same thing, flattened
   Some(8).and_then(half)  = Some(4)            <- one layer
   Some(8).and_then(half).and_then(half) = Some(2)
   Some(7).and_then(half)  = None              <- short-circuits

3. The SAME shape on a different type
   parse("4").and_then(recip)   = Ok(0.25)
   parse("0").and_then(recip)   = Err("no reciprocal of zero")
   parse("x").and_then(recip)   = Err("\"x\": invalid digit found in string")

4. `?` is the same chain, written as statements
   recip_of("4") = Ok(0.25)
   recip_of("0") = Err("no reciprocal of zero")

5. And Vec does it too — `flat_map` is the same operation
   [1,2,3].flat_map(|n| [n, n*10]) = [1, 10, 2, 20, 3, 30]

6. The three laws, checked
   left identity   Some(8).and_then(half) == half(8)          true
   right identity  Some(8).and_then(Some) == Some(8)          true
   associativity   (m>>=f)>>=g == m>>=(\x -> f x >>= g)       true

   all three hold — which is what makes it a monad rather than just a method

The ones you already use

Type wrap chain What the "context" is
Option<T> Some and_then it might be absent
Result<T, E> Ok and_then it might have failed, and why
Vec<T> / iterators vec![x] / once flat_map there might be many
Future<T> async { x } .await in an async block it is not ready yet

Read the right-hand column and the pattern is clear: each type is a value plus a complication, and and_then is "do the next step, and keep handling the complication for me." That is what people mean by the famously unhelpful "programmable semicolon."

? is do-notation

This is the part worth carrying away, because it makes the abstraction concrete.

Haskell has do notation: syntax that turns a chain of >>= (its and_then) into statements that read imperatively. Rust has the same thing, spelled ?:

// what you write
fn recip_of(s: &str) -> Result<f64, String> {
    let n = parse(s)?;
    let r = recip(n)?;
    Ok(r)
}

// what it means
fn recip_of(s: &str) -> Result<f64, String> {
    parse(s).and_then(recip)
}

Both short-circuit on the first failure and neither mentions the failure path. Every ? you have ever written was a monadic bind with the plumbing hidden — which is why ? feels like it is doing something profound and looks like it is doing nothing.

The three laws

A type with an and_then is only a monad if the operation behaves sensibly. Three rules, all checked in the output above:

  • Left identitywrap(x).and_then(f) == f(x). Wrapping and immediately chaining is the same as just calling.
  • Right identitym.and_then(wrap) == m. Chaining a step that only rewraps changes nothing.
  • Associativitym.and_then(f).and_then(g) == m.and_then(|x| f(x).and_then(g)). How you group the chain does not matter.

These are not ceremony. Associativity is what lets you refactor a long ? chain into helper functions without changing behaviour — the thing you rely on daily without checking.

Why Rust does not say the word

Two reasons, and only one is about taste.

The technical one: Rust cannot express it. A Monad trait would need to be generic over a type constructorM where M<T> is a type — and Rust has no higher-kinded types. You can write trait Monad<T>; you cannot write the trait that says "Option itself, for any T". So there is no impl Monad for Option, and there cannot be one today. Each type gets its own and_then with the same shape, and the shape is a convention rather than a trait the compiler knows about.

The cultural one: the name costs more than it pays. Naming the abstraction would mean every beginner meets the word on day two, and the word has a decades-long reputation for producing tutorials that explain burritos. Not naming it means Option::and_then is documented as "call this if there is a value" — true, sufficient, and learnable in one sentence.

The trade is real: because there is no trait, nobody can write a function generic over "any monad", and every new wrapper type re-implements the same method by hand. Rust took the beginner's side, and on the evidence of how many people use ? happily without ever hearing the word, correctly.

Where the analogy stops

  • Option and Result are not interchangeable even though both are monads. ok_or and ok convert; and_then does not bridge them.
  • ? is not fully general. It works on types implementing Try, which is Option, Result and ControlFlow — not Vec, whose bind is flat_map and has no statement syntax.
  • Iterators are lazier than the law suggests. flat_map builds an adaptor rather than a value, so the laws hold about what it yields, not about when work happens.

If you are coming from another language

  • Haskell — this is >>=, return, and do, with the trait removed because Rust cannot express it. If you want the version with the abstraction, that is the side quest, and it is worth taking.
  • Python — you have met the shape without the name too: chained .get() calls that give up on the first None, or itertools.chain.from_iterable flattening what a comprehension nested. The difference is that Rust's version is in the type, so the compiler makes you handle the empty case rather than raising later.
  • ABAP — no counterpart, and the gap is instructive: the ABAP pattern is sy-subrc checked after every call, which is exactly the manual short-circuiting ? automates. A chain of ? is the IF sy-subrc <> 0. RETURN. ENDIF. ladder collapsed into one character.

See also

  • Option vs Result — the two you use most, and how to choose
  • OPTION.md — every lesson on Option, in reading order
  • Why Haskell, for Rust's sake — where to go if this page made you curious rather than satisfied
  • Tris Oaten's framing, which this page agrees with: one of Rust's smartest decisions is to use monads but not call them monads

Po polsku

„Monada” (monad) to jedno z niewielu pojęć, które po polsku mają ustaloną, powszechnie używaną nazwę — i właśnie dlatego bywa trudniejsza dla polskiego czytelnika niż dla angielskiego. Polskie teksty o monadach przychodzą prawie zawsze ze strony Haskella, Scali albo F#, zaczynają od definicji kategorialnej i zostawiają wrażenie, że jest tam coś do zrozumienia poza metodą. W Ruscie kolejność jest odwrotna: z and_then korzysta się miesiącami, zanim ktokolwiek poda nazwę. A cała definicja mieści się w dwóch punktach — sposób na opakowanie zwykłej wartości (Some, Ok, vec![x]) i and_then, które łączy funkcje zwracające to samo opakowanie, nie piętrząc warstw. Widać to na jednej linijce: Some(8).map(half) daje Some(Some(4)), czyli „może może liczba”, co nie znaczy nic więcej niż „może liczba”; and_then to ten sam map z natychmiastowym spłaszczeniem.

Najważniejszy wniosek tej strony brzmi: ? to właśnie do-notation. let n = parse(s)?; i parse(s).and_then(recip) to ten sam program — obie wersje przerywają na pierwszej porażce i żadna nie wspomina o ścieżce błędu. Warto też wiedzieć, po co są trzy prawa monad, bo w polskich materiałach zwykle wyglądają na formalność: łączność (associativity) jest dokładnie tym, co pozwala rozbić długi łańcuch ? na funkcje pomocnicze bez zmiany zachowania. Uwaga na granicę — ? działa na typach z cechą (trait) Try, czyli Option, Result i ControlFlow; wektor też jest monadą, ale jego and_then nazywa się flat_map i nie ma dla niego składni instrukcyjnej.

Rust nie wypowiada tego słowa z dwóch powodów i tylko jeden jest kwestią gustu. Techniczny: żeby napisać trait Monad, trzeba by być generycznym po konstruktorze typu — po samym Option, a nie po Option<T> — a Rust nie ma typów wyższych rodzajów (higher-kinded types). Nie ma więc impl Monad for Option i dziś być nie może, przez co wspólny kształt and_then jest konwencją, a nie cechą, którą zna kompilator. Kulturowy: nazwa kosztowałaby więcej, niż daje — dokumentacja mówi po prostu „wywołaj to, jeśli wartość jest”, i to wystarcza. Praktyczna wskazówka do szukania: hasło „monada” po polsku prawie na pewno wyrzuci wyniki o Haskellu, więc pytania o Rusta zadawaj po angielsku.

Szukaj po polsku: monada · monada w programowaniu · konstruktor typu · rust and_then vs map · rust question mark operator Try · rust higher-kinded types