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:
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 identity —
wrap(x).and_then(f)==f(x). Wrapping and immediately chaining is the same as just calling. - Right identity —
m.and_then(wrap)==m. Chaining a step that only rewraps changes nothing. - Associativity —
m.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 constructor — M 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¶
OptionandResultare not interchangeable even though both are monads.ok_orandokconvert;and_thendoes not bridge them.?is not fully general. It works on types implementingTry, which isOption,ResultandControlFlow— notVec, whose bind isflat_mapand has no statement syntax.- Iterators are lazier than the law suggests.
flat_mapbuilds 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, anddo, 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 firstNone, oritertools.chain.from_iterableflattening 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-subrcchecked after every call, which is exactly the manual short-circuiting?automates. A chain of?is theIF sy-subrc <> 0. RETURN. ENDIF.ladder collapsed into one character.
See also¶
OptionvsResult— 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