赞助商LobeHubLobeHub了解更多
ddshfind
登录

单子:一种"盒子"的设计模式

先说结论,让你心里有底:单子不是一个高深的数学对象,它就是一个朴素的编程模式——"把值装进盒子里,规定只能用窗户取用"。 你早就会用了,只是不知道它叫这个名字。

第 1 步:为什么要"盒子"?

先看一个你天天遇到的烦恼——判空地狱

function 查朋友(id) {
  const user = users[id];
  if (user === undefined) return '查无此人';    // ① 判空
  const friend = friends[user];
  if (friend === undefined) return '这人没朋友'; // ② 又判空
  return friend;
}

每加一步"可能失败"的操作,就得加一个 if。代码越长,判空越多,忘掉一个就炸。盒子(单子)就是来消灭这些 if 的。

第 2 步:造一个最简单的盒子——Maybe 盒

这个盒子只有两种状态:装着值空的

function Just(value)  { return { type: 'just',    value }; }  // 有值
function Nothing()    { return { type: 'nothing' }; }          // 空

就这么简单——一个对象,加一个标记,没有任何玄学。

第 3 步:盒子的规矩——不准直接抠值

盒子最关键的设计:你不能把里面的值直接拿出来。想用里面的值,只能通过"窗户":

// 窗户 1:map —— 盒子里有值,就用 f 处理,再装回盒子;没值就原样传下去
function map(box, f) {
  if (box.type === 'nothing') return Nothing();
  return Just(f(box.value));
}

// 窗户 2:flatMap —— 单子的灵魂!f 返回的是"一个新盒子"
function flatMap(box, f) {
  if (box.type === 'nothing') return Nothing();
  return f(box.value);          // f 返回盒子 → 直接返回 → 天然"压平"
}

注意看 mapflatMap 里那行判空逻辑——判空逻辑被写进窗户里了,你以后再也不用手写判空!

概念对照:Just(x) 就是论文里的 η (eta)——"把纯值装进盒子";flatMap 就是论文里的 μ (mu) 和 bind——"把盒子里套盒子压平"。

第 4 步:用它来解决判空地狱

回到"查用户 → 查朋友"场景,现在每一步都返回盒子

function findUser(id)   { return users[id]   ? Just(users[id])   : Nothing(); }
function findFriend(u)  { return friends[u]  ? Just(friends[u])  : Nothing(); }

// 链式调用:零个 if!
function getFriendOf(id) {
  return flatMap(findUser(id), findFriend);
}

运行结果:

调用结果含义
getFriendOf(1)Just('小刚')正常找到
getFriendOf(999)Nothing查无此人,空自动往下传,不炸
getFriendOf(2)Just('小丽')正常找到

神奇在哪? 用户不存在时,findUser 返回空盒子;flatMap 一看是空的,直接不调用 findFriend,把空传下去。判空逻辑被盒子自动接管了——你想漏都漏不掉。而且串联多少层都行,代码结构永远一样:

const deep = flatMap(flatMap(findUser(1), findFriend), findFriend2);
// → Just('大壮')

第 5 步:惊喜揭晓——你天天在用单子,它叫 Promise

盒子(Maybe)Promise都是"盒子"
Just(5)Promise.resolve(5)把普通值装进盒子
map(box, f)promise.then(f)在盒子里变换值
flatMap(box, f)promise.then(f)(f 返回 Promise)盒子套盒子,自动压平
空盒子传下去rejected Promise 传下去失败自动传播,不炸
box.value(禁止)await(仅语言级允许)打开盒子的唯一途径

唯一的区别是盒子里"附加的东西"不同

  • Maybe 盒子附带的:"这个值可能不存在"
  • Promise 盒子附带的:"这个值要等一会儿,而且可能失败"

盒子本身(设计模式)一模一样。 这就是为什么说"你会写 .then 链,你就已经在用单子了"。

(可能为空 / 可能还没到)Just(x) / resolvemap / flatMap在盒子里操作规则:不能直接把值抠出来Promise 就是这种盒子

η 装值、μ 压平——盒子的规矩让副作用显式、可控、可组合

单子和这篇论文有什么关系?

一句话:单子是"函数式语言处理副作用"的标准工具——把副作用装进盒子,副作用就变得显式、可控、可组合。

而这篇论文的野心是:把"副作用"从"编译时类型系统管的静态概念",升级成"运行时可以真正撤销的动态机制"(还记得吗?可回退效应)。所以论文提到单子,只是给"效应"这个概念找了个理论出身——你不需要会推导单子定律,只需要知道"单子 = 装副作用的盒子"就够了。

💡 动手试试:仓库根目录的 monad-demo.js 是一份可运行的演示代码,node monad-demo.js 就能看到上面所有例子真实运行的结果。

自测题 · 单子

完成作答后点击「提交答案」,可以查看对错与解析。

1. 单子(Monad)最朴素的理解是什么?
2. Maybe 盒子(Just / Nothing)解决的核心痛点是什么?
3. 在盒子模型里,Promise.resolve(x) 对应什么?
4. 论文引入单子,主要目的是什么?