Skip to content

函数组合(Composition):f(g(x)) 的优雅写法 compose(f, g)(x)

——数学中的 f ∘ g 在 JS 中的实现与结合律优势

“组合不是‘先做这个,再做那个’,
而是将两个变换融合为一个新的、更高级的变换。”

一、从 f(g(x))compose(f, g)(x):一场语法革命

传统写法:嵌套调用(由内而外)

js
const result = toUpper(trim(reverse(user.name)))

阅读顺序是:reverse → trim → toUpper,但代码是从右往左写的,认知负荷高

函数组合:管道式(由外而内)

js
const process = compose(toUpper, trim, reverse);
const result = process(user.name);

现在你可以从左到右理解数据流: name → reverse → trim → toUpper → result

二、什么是函数组合?—— 数学中的 f ∘ g

在数学中,函数组合定义为:

(f ∘ g)(x) = f(g(x))

它表示:先应用 g,再将结果传给 f

在 JS 中实现 compose

js
const compose = (...fns) => (x) =>
  fns.reduceRight((acc, fn) => fn(acc), x);

使用示例

js
const trim = s => s.trim();
const toUpper = s => s.toUpperCase();
const exclaim = s => s + '!';

const shout = compose(exclaim, toUpper, trim);
shout('  hello  '); // 'HELLO!'

执行顺序:trim → toUpper → exclaim(从右到左)。

三、pipe:更符合直觉的左到右组合

js
const pipe = (...fns) => (x) =>
  fns.reduce((acc, fn) => fn(acc), x);
js
const shout = pipe(trim, toUpper, exclaim);
shout('  hello  '); // 'HELLO!'

pipe 更符合阅读习惯:数据从左流向右。

推荐:业务代码用 pipe,数学推导用 compose

四、函数组合的核心优势

1. 抽象出“数据流”模式

js
const processUser = pipe(
  validate,
  enrichProfile,
  calculateScore,
  saveToDB
);

你不是在写“一步步操作”,而是在声明“用户处理流程”是什么

2. 可复用的变换单元

每个函数都是独立的:

  • validate 可用于注册、登录
  • enrichProfile 可用于导入用户
  • calculateScore 可用于推荐系统

组合让它们像乐高一样可拼装。

3. 支持热插拔与调试

js
// 轻松插入日志
const withLog = (label, f) => x => {
  console.log(label, x);
  return f(x);
};

const processUser = pipe(
  withLog('input', identity),
  validate,
  withLog('after validate', identity),
  enrichProfile,
  calculateScore,
  saveToDB
);

无需修改核心逻辑,即可插入监控。

五、结合律(Associativity):组合的“代数优势”

这是函数组合最强大的数学特性:

(f ∘ g) ∘ h ≡ f ∘ (g ∘ h)

在 JS 中:

js
compose(compose(f, g), h) === compose(f, compose(g, h))

意味着你可以任意分组:

js
// 方式 1:先组合前两个
const step1 = compose(enrichProfile, validate);
const process = compose(saveToDB, calculateScore, step1);

// 方式 2:先组合后两个
const step2 = compose(calculateScore, enrichProfile);
const process = compose(saveToDB, step2, validate);

// 结果完全等价

工程价值:

  • 并行开发:团队可独立开发 validate+enrichscore+save 模块
  • 模块化测试:可单独测试子组合
  • 自由重构:无需担心组合顺序破坏逻辑

六、组合的类型约束:必须“类型对齐”

函数组合要求: 前一个函数的输出类型,必须匹配后一个函数的输入类型。

text
// 类型对齐
String → String → String → String
 trim     toUpper    exclaim

// 类型错位
Number → String → Number
  add5     toUpper    add10  // toUpper 返回 string,add10 要 number

这是组合的“契约”,也迫使你设计更清晰的接口

七、高级应用:组合异步函数

js
const fetchUser = id => fetch(`/api/users/${id}`).then(res => res.json());
const processUser = user => ({ ...user, processed: true });
const saveToCache = user => localStorage.setItem('user', JSON.stringify(user));

// 组合异步流程
const loadAndCache = pipe(
  fetchUser,
  map(processUser), // 注意:Promise 需要 map
  chain(saveToCache) // 或 then
);

在 FP 库中,mapchainflatMap)让异步函数也能安全组合。

八、工程实践:写出可组合的函数

1. 单一职责

每个函数只做一件事,输入输出清晰。

2. 纯函数优先

避免副作用,确保可缓存、可推理。

3. 使用 pipe 构建数据流

js
const processOrder = pipe(
  validateOrder,
  calculateTax,
  applyDiscounts,
  chargePayment,
  shipOrder
);

4. 避免“组合地狱”

js
// 过度嵌套
compose(f, compose(g, h), compose(i, j))

// 使用 pipe 或分步命名
const preProcess = pipe(g, h);
const postProcess = pipe(i, j);
const process = pipe(preProcess, f, postProcess);

结语:函数组合是“函数式编程的终极武器”

它让代码从“指令序列”升华为“代数表达式”

当你能写出:

js
const userJourney = pipe(
  register,
  verifyEmail,
  completeProfile,
  makeFirstPurchase,
  earnLoyaltyPoints
);

你已经不再“写代码”,而是在用函数语言描述业务的数学结构

这才是 compose(f, g)(x) 的真正意义。