Skip to content

函数组合 (Function Composition)

1. 概念

函数组合是将两个或多个函数合并为一个新函数的过程。它是函数式编程的核心支柱之一。

如果要把数据 x 通过函数 f 处理,再通过函数 g 处理,我们可以写成 g(f(x))。 函数组合就是将 fg 结合起来,生成一个新的函数 h,使得 h(x) = g(f(x))

$$ (g \circ f)(x) = g(f(x)) $$

"组合就像是用乐高积木搭建城堡。每个小函数就是一块积木,组合就是把它们扣在一起的过程。"

2. Compose vs Pipe

在 JavaScript 中,函数组合通常有两种实现方式,主要区别在于执行顺序:

Compose (从右向左)

符合数学上的组合定义 $(g \circ f)$。

javascript
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);

// 执行顺序:f3 -> f2 -> f1
const myFunc = compose(f1, f2, f3);

Pipe (从左向右)

符合数据流动的直觉(管道)。

javascript
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);

// 执行顺序:f1 -> f2 -> f3
const myFunc = pipe(f1, f2, f3);

3. 结合律 (Associativity)

函数组合满足结合律,这与加法或乘法类似。

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

这意味着只要顺序保持一致,我们如何对函数进行分组(括起来)并不影响最终结果。这为代码重构和提取公共逻辑提供了理论基础。

4. 为什么使用函数组合?

  1. 消除洋葱代码: 避免 h(g(f(x))) 这种层层嵌套的写法,代码更扁平。
  2. Point-free: 鼓励写出不显式操作数据的代码,关注数据转换的流程。
  3. 更高层次的抽象: 通过组合小的、通用的纯函数,构建出复杂的业务逻辑。

5. 调试困难与解决方案

函数组合的一个缺点是数据隐藏在管道中,难以直接 console.log。 我们可以编写一个 trace 辅助函数来查看中间状态:

javascript
const trace = tag => x => {
  console.log(tag, x);
  return x;
};

const process = pipe(
  input,
  trace('after input'), // 打印中间结果
  processA,
  trace('after processA'), // 打印中间结果
  output
);

6. 常用库

在实际开发中,可以使用成熟的库来处理柯里化和函数组合:

  • Ramda: R.compose, R.pipe
  • Lodash/fp: fp.compose, fp.pipe

返回函数式编程目录

MIT Licensed | Keep Learning.