Skip to content

函子(Functor):map 背后的统一抽象

——任何能 map 的类型(Array、Maybe、Observable)都是函子

map 不是数组的特权,
它是所有可变换容器的通用语言。”

一、从具体到抽象:我们都在用 map,但你真的懂它吗?

我们见过这些:

js
[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 把函数“注入”盒子内部去处理它
js
// 盒子:Maybe
const maybeValue = Maybe.of(42);
const mapped = maybeValue.map(x => x * 2); // 还是 Maybe,但值变了

三、函子的两大数学定律

要称为“函子”,必须遵守两个定律:

1. 恒等律(Identity Law)

F.map(x => x) 应该等于 F

js
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)))

js
[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 - 最熟悉的函子

js
[1,2,3].map(x => x + 1) // [2,3,4]
  • 容器:数组
  • map:对每个元素应用函数

2. Maybe - 处理可能为空的值

js
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 - 处理成功或失败

js
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) - 异步事件流

js
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,但本质是函子:

js
Promise.resolve(5)
  .then(x => x * 2) // Promise{10}

注意:then 实际是 Monad(支持链式异步),但也是函子。

五、实现一个自己的函子

示例:IO 函子(封装副作用)

js
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 变换它。

js
container.map(transform).map(validate).map(format)

一行代码,适用于 ArrayMaybePromise...

2. 隔离副作用

函子让你在“安全区”内操作值,而不触发实际计算(如 IO、Promise)。

3. 提升代码复用

写一个函数处理“盒子里的值”,它就能用于所有函子类型。

js
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 从一个数组方法,升华为一种普适的变换哲学

当你看到:

js
container.map(f)

你不再关心 container 是数组、Promise 还是自定义类型。
你只关心:有一个值在上下文中,我要用 f 变换它

这就是抽象的力量。