函数组合 (Function Composition)
1. 概念
函数组合是将两个或多个函数合并为一个新函数的过程。它是函数式编程的核心支柱之一。
如果要把数据 x 通过函数 f 处理,再通过函数 g 处理,我们可以写成 g(f(x))。 函数组合就是将 f 和 g 结合起来,生成一个新的函数 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. 为什么使用函数组合?
- 消除洋葱代码: 避免
h(g(f(x)))这种层层嵌套的写法,代码更扁平。 - Point-free: 鼓励写出不显式操作数据的代码,关注数据转换的流程。
- 更高层次的抽象: 通过组合小的、通用的纯函数,构建出复杂的业务逻辑。
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