函子(Functor):map 背后的统一抽象
——任何能 map 的类型(Array、Maybe、Observable)都是函子
“map 不是数组的特权,
它是所有可变换容器的通用语言。”
一、从具体到抽象:我们都在用 map,但你真的懂它吗?
我们见过这些:
[1,2,3].map(x => x * 2) // Array
Promise.resolve(5).then(x => x+1) // Promise (异步 map)它们行为相似,但被认为是“不同机制”。
函子揭示了真相:
Array 是函子
Promise 是函子
Maybe、Either、Observable 都是函子
只要一个类型实现了 map,并满足两个数学定律,它就是函子。
二、什么是函子?—— 容器 + 变换
核心定义:
函子是一个可以被映射(mapped over)的上下文(Context)或容器(Container)。
你可以把它想象成一个“盒子”:
- 盒子里装着一个值
- 你不能直接碰值
- 你只能用
map把函数“注入”盒子内部去处理它
// 盒子:Maybe
const maybeValue = Maybe.of(42);
const mapped = maybeValue.map(x => x * 2); // 还是 Maybe,但值变了三、函子的两大数学定律
要称为“函子”,必须遵守两个定律:
1. 恒等律(Identity Law)
F.map(x => x) 应该等于 F
const arr = [1,2,3];
arr.map(x => x); // [1,2,3] —— 没变
const p = Promise.resolve(5);
p.then(x => x); // Promise{5} —— 等价如果 map 改变了结构,就不是函子。
2. 合成律(Composition Law)
F.map(f).map(g) ≡ F.map(x => g(f(x)))
[1,2,3]
.map(x => x * 2) // [2,4,6]
.map(x => x + 1) // [3,5,7]
// 等价于
[1,2,3].map(x => (x * 2) + 1) // [3,5,7]这个定律保证 map 是可组合的,不会产生副作用。
四、常见函子类型
1. Array - 最熟悉的函子
[1,2,3].map(x => x + 1) // [2,3,4]- 容器:数组
map:对每个元素应用函数
2. Maybe - 处理可能为空的值
const Maybe = {
of: value => ({
value,
map: f => value == null ? Maybe.of(null) : Maybe.of(f(value))
})
};
const user = { name: 'Alice', age: null };
const age = Maybe.of(user.age).map(x => x + 1); // Maybe.of(null)避免 null 错误,优雅处理缺失值。
3. Either - 处理成功或失败
const Either = {
of: value => ({
value,
isRight: true,
map: f => Either.of(f(value))
}),
left: value => ({
value,
isRight: false,
map: () => Either.left(value) // 失败时跳过 map
})
};
// 成功路径
Either.of(5).map(x => x * 2); // Right(10)
// 失败路径
Either.left('error').map(x => x * 2); // Left('error')用于错误处理,替代 try/catch。
4. Observable (RxJS) - 异步事件流
import { of } from 'rxjs';
import { map } from 'rxjs/operators';
of(1,2,3).pipe(
map(x => x * 2)
).subscribe(console.log); // 2,4,6时间上的函子,对事件流进行变换。
5. Promise - 异步值
虽然叫 then,但本质是函子:
Promise.resolve(5)
.then(x => x * 2) // Promise{10}注意:then 实际是 Monad(支持链式异步),但也是函子。
五、实现一个自己的函子
示例:IO 函子(封装副作用)
const IO = fn => ({
fn,
map: f => IO(() => f(fn())), // 包装新函数
run: () => fn() // 执行
});
// 使用
const readUrl = url => () => fetch(url).then(r => r.text());
const ioRead = IO(readUrl('/api/data'));
const processed = ioRead
.map(text => text.toUpperCase())
.map(text => text.slice(0, 100));
// 延迟执行
processed.run().then(show);IO 让副作用变得可组合、可推理。
六、为什么函子重要?
1. 统一接口
无论数据在数组里、异步中、还是可能为空,你都可以用 map 变换它。
container.map(transform).map(validate).map(format)一行代码,适用于 Array、Maybe、Promise...
2. 隔离副作用
函子让你在“安全区”内操作值,而不触发实际计算(如 IO、Promise)。
3. 提升代码复用
写一个函数处理“盒子里的值”,它就能用于所有函子类型。
const double = x => x * 2;
// 可用于:
[1,2].map(double)
Maybe.of(5).map(double)
Promise.resolve(3).then(double)七、工程实践建议
使用场景
- 处理异步(Promise、Observable)
- 避免 null/undefined 错误(Maybe)
- 构建 DSL 或配置化流程
- 函数式状态管理
工具推荐
- Ramda:提供
Maybe,Either - Sanctuary:类型安全的 FP 库
- fp-ts:TypeScript 中的强大函子支持
- RxJS:响应式编程函子
结语:函子是“函数式编程的多态”
它让
map从一个数组方法,升华为一种普适的变换哲学。
当你看到:
container.map(f)你不再关心 container 是数组、Promise 还是自定义类型。
你只关心:有一个值在上下文中,我要用 f 变换它。
这就是抽象的力量。