Skip to content

🔥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 → B

Applicative:

text
F.ap(F<B>) // 把一个在上下文中的函数,应用到另一个上下文

必备方法:

  1. of(x):提升值到上下文(同 Monad)
  2. 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 提升任意函数