🔥Applicative:介于 Functor 与 Monad 之间的抽象
——支持多个上下文值的函数应用(如 liftA2(add, Maybe(1), Maybe(2)))
“Functor 映射单个值,
Monad 序列依赖操作,
Applicative 处理独立但带上下文的并行计算。”
一、问题:Functor 的局限
我们已知函子(Functor)能用 map 变换容器内的值:
js
const add = (a, b) => a + b;
const inc = x => x + 1;
Maybe.of(5).map(inc); // Just(6)但如果函数本身也在上下文中呢?
js
const maybeAdd = Maybe.of(add);
const a = Maybe.of(2);
const b = Maybe.of(3);
// 如何计算 `maybeAdd(a, b)`?map 无法解决:
js
a.map(maybeAdd) // ❌ 得到的是 Maybe<function>我们需要一种方式,把一个“在上下文中的函数”应用到“在上下文中的参数”。
这就是 Applicative 的使命。
二、Applicative 的核心:ap 方法
函子(Functor):
js
F.map(f) // f: A → BApplicative:
text
F.ap(F<B>) // 把一个在上下文中的函数,应用到另一个上下文必备方法:
of(x):提升值到上下文(同 Monad)ap(otherFunctor):应用函数
示例:Maybe 的 Applicative 实现
js
const Maybe = {
of: value => value == null ? Nothing() : Just(value),
ap: (fn, val) => fn.map(f => f(val.value))
};
const Just = value => ({
map: f => Maybe.of(f(value)),
ap: other => Maybe.of(value(other.value)), // other 是 Just(arg)
toString: () => `Just(${value})`
});
const Nothing = () => ({
map: () => Nothing(),
ap: () => Nothing(),
toString: () => 'Nothing'
});使用 ap 计算 add(Maybe(2), Maybe(3))
js
const add = a => b => a + b; // 柯里化!
const maybeAdd = Maybe.of(add);
const a = Maybe.of(2);
const b = Maybe.of(3);
// 步骤1:apply 到第一个参数
const partial = maybeAdd.ap(a); // Just(b => 2 + b)
// 步骤2:apply 到第二个参数
const result = partial.ap(b); // Just(5)
console.log(result); // Just(5)三、liftA2:提升二元函数到 Applicative 上下文
手动写 ap 链太繁琐,所以有 liftA2:
js
const liftA2 = (f, fa, fb) =>
fa.map(f).ap(fb);使用:
js
const result = liftA2(
add, // (a,b) => a+b
Maybe.of(2),
Maybe.of(3)
); // Just(5)如果任一为 Nothing,结果就是 Nothing —— 安全且可组合。
四、为什么 Applicative 在 Functor 和 Monad 之间?
| 能力 | 描述 | 典型方法 |
|---|---|---|
| Functor | 映射单个上下文值 | map |
| Applicative | 应用多个独立上下文值 | ap, liftA2 |
| Monad | 序列依赖操作(后一步依赖前一步) | chain, flatMap |
关键区别:
- Applicative:所有计算独立,并行
- Monad:计算有依赖,串行
五、真实场景:表单验证(Applicative vs Monad)
场景:验证用户名和邮箱
Monad 方式(串行,早停)
js
validateName(name)
.chain(validName =>
validateEmail(email)
.map(validEmail => ({ name: validName, email: validEmail }))
);如果 name 无效,不会验证 email —— 无法收集所有错误。
Applicative 方式(并行,累积)
js
const liftA2 = (f, a, b) => a.map(f).ap(b);
const allErrors = liftA2(
name => email => ({ name, email }),
validateNameWithAccumulate(name), // 返回 AllErrors<Array>
validateEmailWithAccumulate(email) // 返回 AllErrors<Array>
);即使 name 无效,仍会验证 email,最终返回所有错误列表。
这才是用户体验友好的验证!
六、Promise 也是 Applicative!
Promise.all 就是 Applicative!
js
const p1 = fetch('/user');
const p2 = fetch('/posts');
const p3 = fetch('/config');
Promise.all([p1, p2, p3])
.then(([user, posts, config]) => combine(user, posts, config));Promise.all 等待所有 Promise 独立完成,然后合并结果 —— 这正是 Applicative 的行为。
对比:
Promise.then(Monad):串行,依赖Promise.all(Applicative):并行,独立
七、Applicative 的两大定律
要称为 Applicative,必须满足:
1. 恒等律
F.of(id).ap(v) ≡ v
js
Maybe.of(x => x).ap(Maybe.of(5)) === Maybe.of(5)2. 组合律
F.of(compose).ap(u).ap(v).ap(w) ≡ u.ap(v.ap(w))
保证函数组合在上下文中依然成立。
八、工程价值
适用场景
- 并行异步请求(
Promise.all) - 表单验证(收集所有错误)
- 配置加载(多个独立文件)
- 日志聚合(多个来源)
优势
- 性能:并行执行,无依赖等待
- 健壮性:不因一个失败而中断其他
- 可组合性:
liftA2,liftA3提升任意函数