JavaScript 中的函子(Functor)与单子(Monad):Maybe 与 Either Monad 的实战错误处理
大家好,欢迎来到今天的编程技术讲座。今天我们不讲“Hello World”,也不讲“闭包陷阱”,而是深入探讨一个在函数式编程中非常重要的概念——函子(Functor)和单子(Monad),并聚焦于两个最实用的类型:Maybe 和 Either,它们能帮我们在 JavaScript 中优雅地处理错误。
如果你曾经写过这样的代码:
const user = getUserById(id);
if (user && user.profile) {
return user.profile.name;
} else {
return "Unknown User";
}
或者更糟的情况:
try {
const result = riskyOperation();
return result.data;
} catch (e) {
return null;
}
你会发现这种模式重复、冗长、容易出错。而这就是我们今天要解决的问题:如何用函数式的方式统一处理“可能失败”的操作?
一、什么是 Functor(函子)?
首先我们要理解什么是 函子(Functor)。
定义:
函子是一个包装了值的对象,它提供了一个
.map()方法,可以对内部的值进行变换,同时保持结构不变。
换句话说,只要你的类型支持 .map(fn),并且满足两个数学性质(恒等律和组合律),你就有了一个函子。
在 JavaScript 中的例子:
// 简单的 Maybe 函子(伪实现)
class Maybe {
constructor(value) {
this.value = value;
}
map(fn) {
if (this.value === null || this.value === undefined) {
return new Maybe(null);
}
return new Maybe(fn(this.value));
}
static of(value) {
return new Maybe(value);
}
}
// 使用示例
const maybeUser = Maybe.of({ name: "Alice", age: 25 });
const maybeName = maybeUser.map(user => user.name); // Maybe("Alice")
const maybeNull = Maybe.of(null).map(user => user.name); // Maybe(null)
✅ 这里我们实现了 Maybe 的 map,当值为 null/undefined 时自动返回空包装,避免后续调用出错。
这比手动检查 if (user) 更干净,也更容易链式调用。
| 操作 | 常规写法 | Maybe 函子写法 |
|---|---|---|
| 获取用户姓名 | user && user.name |
Maybe.of(user).map(u => u.name) |
| 处理嵌套属性 | user?.profile?.name |
Maybe.of(user).map(u => u.profile).map(p => p.name) |
👉 函子的好处在于:你可以放心地连续 map,不用担心中间某个值是 null 或 undefined。
但注意!函子只能处理“值存在与否”的情况,不能处理“错误类型”或“多种失败场景”。比如网络请求失败、数据库连接异常、参数格式不对……这些都属于“不同种类的失败”。
这就引出了下一个概念:单子(Monad)。
二、什么是 Monad(单子)?
定义:
单子是一种特殊的函子,它还提供了一个
.chain()或.flatMap()方法,允许你在值上执行带有副作用的操作(如异步、错误处理),并自动展开嵌套结构。
简单来说:
- 函子:你可以在里面安全地做转换(map)。
- 单子:你可以在里面安全地做流程控制(chain / flatMap),甚至跨多个失败路径。
为什么需要单子?
因为很多时候,你不是简单地想“如果为空就返回 null”,而是想:
- 如果 A 成功 → 执行 B;
- 如果 A 失败 → 返回错误信息;
- 如果 B 又失败 → 返回另一个错误信息;
这就是 Either 单子的作用!
三、Maybe vs Either:谁更适合错误处理?
| 特性 | Maybe | Either |
|---|---|---|
| 用途 | 表示“可能有值也可能没有” | 表示“要么成功(Right),要么失败(Left)” |
| 类型 | Maybe<T> |
Either<Error, T> |
| 是否携带错误信息 | ❌ 否(只关心是否为空) | ✅ 是(可区分具体错误) |
| 链式操作 | ✅ 支持 map | ✅ 支持 chain(更强的流程控制) |
| 实战场景 | 空值判断、默认值 | API 调用、数据验证、文件读取等 |
🎯 推荐使用场景:
- Maybe:当你只需要知道“有没有值”,不需要关心为什么没值。
- Either:当你希望明确区分成功和失败,并且能传递具体的错误信息。
四、实战案例:用 Maybe 和 Either 处理真实业务逻辑
假设我们正在开发一个用户管理系统,涉及以下操作:
- 从数据库获取用户(可能不存在)
- 获取用户的地址(可能未设置)
- 发送邮件通知(可能失败)
场景一:使用 Maybe 处理空值
// Maybe 实现(简化版)
class Maybe {
constructor(value) {
this.value = value;
}
map(fn) {
if (this.value == null) return Maybe.of(null);
return Maybe.of(fn(this.value));
}
static of(value) {
return new Maybe(value);
}
}
// 模拟数据库查询
function getUser(id) {
return Maybe.of(users.find(u => u.id === id));
}
function getAddress(userId) {
return getUser(userId).map(user => user.address);
}
function sendEmail(address) {
return address ? Maybe.of(`Email sent to ${address}`) : Maybe.of(null);
}
// 使用链式调用
const result = getUser(123)
.map(u => u.profile)
.map(profile => profile.email)
.map(email => sendEmail(email));
console.log(result.value); // null(因为 profile 不存在)
✅ 这种方式避免了 Cannot read property 'email' of undefined 错误。
场景二:使用 Either 处理多种错误
现在我们升级需求:不仅要处理空值,还要记录错误原因。
// Either 实现(简化版)
class Either {
constructor(leftOrRight) {
this.isLeft = leftOrRight instanceof Left;
this.value = leftOrRight.value;
}
map(fn) {
if (this.isLeft) return this; // 左边不映射
return Either.of(fn(this.value));
}
chain(fn) {
if (this.isLeft) return this;
return fn(this.value);
}
static of(value) {
return new Right(value);
}
static left(error) {
return new Left(error);
}
}
class Left {
constructor(value) {
this.value = value;
}
}
class Right {
constructor(value) {
this.value = value;
}
}
// 模拟复杂业务逻辑
function fetchUser(id) {
if (!id) return Either.left(new Error("ID is required"));
const user = users.find(u => u.id === id);
if (!user) return Either.left(new Error(`User not found for ID: ${id}`));
return Either.of(user);
}
function validateEmail(user) {
if (!user.email) return Either.left(new Error("Email missing"));
return Either.of(user);
}
function sendNotification(user) {
// 模拟发送失败
if (Math.random() > 0.8) {
return Either.left(new Error("Failed to send notification"));
}
return Either.of(`Notified ${user.email}`);
}
// 使用链式调用(chain 替代 map,适合错误传播)
const result = fetchUser(123)
.chain(validateEmail)
.chain(sendNotification);
console.log(result.isLeft ? result.value.message : result.value);
// 输出可能是:"Failed to send notification" 或 "Notified [email protected]"
🎯 这里 chain 允许我们在每个步骤中决定是否继续流程,而不是像 map 那样忽略错误。
💡 重点来了:chain 是单子的核心特性! 它让你可以写出类似下面的结构:
fetchUser(id)
.chain(validateEmail)
.chain(sendNotification)
.fold(
error => console.error("Error:", error.message),
success => console.log("Success:", success)
);
我们可以加一个 fold 方法来统一处理最终结果:
Either.prototype.fold = function(onLeft, onRight) {
return this.isLeft ? onLeft(this.value) : onRight(this.value);
};
这样整个流程就可以清晰地分为两部分:错误处理 和 成功处理。
五、对比总结:Maybe vs Either 的选择指南
| 场景 | 推荐类型 | 理由 |
|---|---|---|
| 数据库查询、字段访问、API 返回值 | Maybe | 只关心是否存在,不需要详细错误信息 |
| 用户输入验证、HTTP 请求、文件操作 | Either | 必须知道失败原因,便于调试和用户体验 |
| 需要链式调用且中间可能失败 | Either | chain 支持错误传播,避免嵌套 try-catch |
| 性能敏感、轻量级场景 | Maybe | 结构简单,无额外开销 |
| 日志追踪、错误分类 | Either | 可以携带丰富的错误对象(如 code、timestamp、context) |
📌 小贴士:
- 如果你发现很多地方都在写
if (x) { ... } else { return null },那就该考虑用Maybe。 - 如果你发现很多地方都在写
try/catch并抛出不同类型异常,那就该考虑用Either。
六、进阶技巧:结合两者构建健壮系统
有时候我们既需要处理“空值”,也需要处理“错误”,怎么办?
可以用 Either 包装 Maybe:
function safeParseJSON(str) {
try {
return Either.of(JSON.parse(str));
} catch (e) {
return Either.left(new Error(`Invalid JSON: ${e.message}`));
}
}
function extractUserId(data) {
if (!data.user) return Either.left(new Error("No user in data"));
return Either.of(data.user.id);
}
// 组合使用
safeParseJSON('{"user": {"id": 456}}')
.chain(extractUserId)
.fold(
err => console.error(err.message),
id => console.log("User ID:", id)
);
这种模式非常适合微服务接口、配置加载器、数据清洗管道等场景。
七、常见误区与注意事项
| 误区 | 正确做法 |
|---|---|
| “我用了 Maybe 就不用写 try/catch 了” | ❌ 不对!Maybe 只能处理 null/undefined,无法捕获运行时异常(如语法错误、网络中断) |
| “Either 的 left 一定要是 Error 对象” | ❌ 不一定!可以是字符串、对象、数字等,只要能表达错误含义即可 |
| “链式调用越多越好” | ❌ 应该合理分层,过度链式会让代码难以阅读。适当封装成函数更有意义 |
| “单子太抽象,我不懂” | ✅ 先从 Maybe 开始练手,再逐步过渡到 Either,慢慢体会其威力 |
八、结语:让错误变得可控,而不是被忽视
JavaScript 的动态性和灵活性带来了便利,但也让错误处理变得混乱。通过引入函子和单子的概念,我们可以:
- 把“空值”变成一种结构化的状态(Maybe)
- 把“错误”变成一种可传递的信息(Either)
- 让整个流程变成纯函数式的链式操作,无需显式 try/catch
这不是为了炫技,而是为了让我们的代码更可预测、可测试、易维护。
记住一句话:
“好的错误处理不是隐藏错误,而是让错误变得可见、可控、可恢复。”
下次当你遇到一堆 if (user) 或 try/catch 时,请问问自己:“能不能用 Maybe 或 Either 来重构?”
你会发现,世界会变得更清晰一点。
谢谢大家!