Monad: A "Box" Design Pattern
Let's start with the takeaway, so you know where you're headed: a monad is not an esoteric mathematical object — it's a plain programming pattern: "put values in a box, and only take them out through the window." You've already been using it; you just didn't know it had this name.
Step 1: Why a "box"?
Here's a frustration you run into every day — null-check hell:
function 查朋友(id) {
const user = users[id];
if (user === undefined) return '查无此人'; // ① null check
const friend = friends[user];
if (friend === undefined) return '这人没朋友'; // ② another null check
return friend;
}
Every time you add a step that "might fail," you have to add another if. The longer the code, the more null checks — forget one and it blows up. The box (monad) exists to eliminate these ifs.
Step 2: Build the simplest box — the Maybe box
This box has only two states: holding a value or empty.
function Just(value) { return { type: 'just', value }; } // has a value
function Nothing() { return { type: 'nothing' }; } // empty
That's all there is to it — an object plus a tag. No mysticism whatsoever.
Step 3: The box's rule — no reaching in for the value
The most critical design decision of the box: you can't take the value out directly. To use the value inside, you can only go through a "window":
// Window 1: map — if the box has a value, process it with f and put it back in the box; if empty, pass it through unchanged
function map(box, f) {
if (box.type === 'nothing') return Nothing();
return Just(f(box.value));
}
// Window 2: flatMap — the soul of a monad! f returns "a new box"
function flatMap(box, f) {
if (box.type === 'nothing') return Nothing();
return f(box.value); // f returns a box → return it directly → naturally "flattened"
}
Notice the null-check line inside map and flatMap — the null-check logic is written into the windows, so you never have to hand-write a null check again!
Concept mapping:
Just(x)is the paper's η (eta) — "wrapping a plain value into a box";flatMapis the paper's μ (mu) and bind — "flattening boxes nested inside boxes."
Step 4: Use it to solve null-check hell
Back to the "find user → find friend" scenario; now every step returns a box:
function findUser(id) { return users[id] ? Just(users[id]) : Nothing(); }
function findFriend(u) { return friends[u] ? Just(friends[u]) : Nothing(); }
// Chained call: zero ifs!
function getFriendOf(id) {
return flatMap(findUser(id), findFriend);
}
Running results:
| Call | Result | Meaning |
|---|---|---|
getFriendOf(1) | Just('小刚') | found normally |
getFriendOf(999) | Nothing | user not found; the empty box passes down automatically, no crash |
getFriendOf(2) | Just('小丽') | found normally |
Where's the magic? When the user doesn't exist, findUser returns an empty box; flatMap sees it's empty and simply skips calling findFriend, passing the emptiness down. The null-check logic is taken over by the box automatically — you can't miss it even if you try. And you can chain as many layers as you want; the code structure is always the same:
const deep = flatMap(flatMap(findUser(1), findFriend), findFriend2);
// → Just('大壮')
Step 5: Surprise reveal — you use a monad every day, and it's called Promise
| Box (Maybe) | Promise | Both are "boxes" |
|---|---|---|
Just(5) | Promise.resolve(5) | wrap a plain value into a box |
map(box, f) | promise.then(f) | transform the value inside the box |
flatMap(box, f) | promise.then(f) (f returns a Promise) | box inside box, flattened automatically |
| empty box passes down | rejected Promise passes down | failure propagates automatically, no crash |
box.value (forbidden) | await (language-level only) | the only way to open the box |
The only difference is what "extra thing" the box carries:
- The Maybe box carries: "this value might not exist"
- The Promise box carries: "this value will take a while, and it might fail"
The box itself (the design pattern) is exactly the same. That's why it's said that "if you can write a .then chain, you're already using monads."
η 装值、μ 压平——盒子的规矩让副作用显式、可控、可组合
What does the monad have to do with this paper?
In one sentence: a monad is the standard tool for "handling side effects in functional languages" — put the side effect into a box, and it becomes explicit, controllable, and composable.
And this paper's ambition is: upgrade "side effects" from "a static concept managed by the compile-time type system" to "a dynamic mechanism that can actually be undone at runtime" (remember? reversible effects). So when the paper mentions monads, it's just giving the concept of "effects" a theoretical pedigree — you don't need to derive the monad laws; you only need to know that "monad = a box that holds side effects."
💡 Try it yourself:
monad-demo.jsin the repo root is a runnable demo; runnode monad-demo.jsto see the real output of all the examples above.
Self-check · Monads
Answer each question, then submit to check your result.
