函数组合(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+enrich和score+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 库中,map 和 chain(flatMap)让异步函数也能安全组合。
八、工程实践:写出可组合的函数
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) 的真正意义。