JavaScript函数式编程—柯里化、组合与管道模式实战|新宇宙博客返回列表高阶操作与管道组合
系统讲解纯函数原则、柯里化实现与应用、偏函数应用、函数组合两种方向及Point-free风格的适用边界
高阶操作与管道组合
函数组合(Composition)和组合子(Combinators)是函数式编程的核心模式。通过将小型、纯粹的函数组合成复杂操作,可以构建高度可复用、可测试的代码。本文覆盖 pipe/compose、柯里化、偏函数、Transducers 等高级组合技术,并探讨 TC39 Pipeline Operator 提案的最新进展。
目录
- 函数组合基础
- pipe 与 compose
- 柯里化与偏应用
- 常用组合子
- Transducers
- Pipeline Operator 提案
- Pointfree 风格
- 实战案例
- 深度追问
- 总结表格
1. 函数组合基础
function processUser(user) {
const validated = validate(user);
const normalized = normalize(validated);
const enriched = enrich(normalized);
return enriched;
}
processUser = (validate, normalize, enrich);
const
pipe
2. pipe 与 compose
function pipe(...fns) {
if (fns.length === 0) return (x) => x;
if (fns.length === 1) return fns[0];
return function piped(...args) {
let result = fns[0].apply(this, args);
for (let i = 1; i < fns.length; i++) {
result = fns[i](result);
}
return result;
};
}
function compose(...fns) {
return pipe(...fns.reverse());
}
const processString = pipe(
str => str.trim(),
str => str.toLowerCase(),
str => str.replace(/\s+/g, '-'),
str => str.slice(0, 50)
);
console.log(processString(' Hello World '));
异步 pipe
function asyncPipe(...fns) {
return async function(...args) {
let result = await fns[0].apply(this, args);
for (let i = 1; i < fns.length; i++) {
result = await fns[i](result);
}
return result;
};
}
const processOrder = asyncPipe(
validateOrder,
async (order) => await checkInventory(order),
async (order) => await processPayment(order),
async (order) => await sendConfirmation(order)
);
3. 柯里化与偏应用
function curry(fn) {
const arity = fn.length;
return function curried(...args) {
if (args.length >= arity) {
return fn.apply(this, args);
}
return function(...moreArgs) {
return curried.apply(this, [...args, ...moreArgs]);
};
};
}
const add = curry((a, b, c) => a + b + c);
console.log(add(1)(2)(3));
console.log(add(1, 2)(3));
console.log(add(1)(2, 3));
console.log(add(1, 2, 3));
const add10 = add(10);
const add10and20 = add(10, 20);
console.log(add10(5, 3));
console.log(add10and20(3));
占位符柯里化
const _ = Symbol('placeholder');
function curryWithPlaceholder(fn) {
const arity = fn.length;
return function curried(...args) {
const complete = args.length >= arity &&
args.slice(0, arity).every(a => a !== _);
if (complete) return fn(...args);
return (...newArgs) => {
const merged = args.map(a => a === _ && newArgs.length ? newArgs.shift() : a);
return curried(...merged, ...newArgs);
};
};
}
const divide = curryWithPlaceholder((a, b) => a / b);
const divideBy2 = divide(_, 2);
console.log(divideBy2(10));
4. 常用组合子
const identity = x => x;
const constant = x => () => x;
const tap = fn => x => { fn(x); return x; };
const flip = fn => (a, b) => fn(b, a);
const once = fn => {
let called = false, result;
return (...args) => {
if (!called) { called = true; result = fn(...args); }
return result;
};
};
const not = fn => (...args) => !fn(...args);
const when = (pred, fn) => x => pred(x) ? fn(x) : x;
const unless = (pred, fn) => x => pred(x) ? x : fn(x);
const processItems = pipe(
tap(items => console.log(`Processing ${items.length} items`)),
items => items.filter(not(item => item.deleted)),
items => items.map(when(
item => item.price > 100,
item => ({ ...item, discounted: true })
))
);
5. Transducers
const mapping = (fn) => (reducer) => (acc, val) => reducer(acc, fn(val));
const filtering = (pred) => (reducer) => (acc, val) => pred(val) ? reducer(acc, val) : acc;
const taking = (n) => (reducer) => {
let count = 0;
return (acc, val) => ++count <= n ? reducer(acc, val) : acc;
};
const xform = compose(
filtering(x => x % 2 === 0),
mapping(x => x * x),
taking(3)
);
function transduce(xform, reducer, init, collection) {
const xReducer = xform(reducer);
return collection.reduce(xReducer, init);
}
const result = transduce(
xform,
(acc, val) => [...acc, val],
[],
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
);
console.log(result);
6. Pipeline Operator 提案
function pipeline(value, ...steps) {
return steps.reduce((acc, step) => step(acc), value);
}
const result = pipeline(
' Hello, World! ',
s => s.trim(),
s => s.toLowerCase(),
s => s.split(' '),
words => words.map(w => w[0].toUpperCase() + w.slice(1)),
words => words.join(' ')
);
console.log(result);
7. Pointfree 风格
const getNames = users => users.map(user => user.name);
const prop = key => obj => obj[key];
const map = fn => arr => arr.map(fn);
const getNamesPointfree = map(prop('name'));
const filter = pred => arr => arr.filter(pred);
const reduce = (fn, init) => arr => arr.reduce(fn, init);
const sort = comparator => arr => [...arr].sort(comparator);
const join = separator => arr => arr.join(separator);
const split = separator => str => str.split(separator);
const getActiveUserEmails = pipe(
filter(prop('active')),
map(prop('email')),
sort((a, b) => a.localeCompare(b)),
join(', ')
);
8. 实战案例
实战案例 1:Express 中间件组合
function composeMiddleware(...middlewares) {
return function(req, res, next) {
let index = -1;
function dispatch(i) {
if (i <= index) throw new Error('next() called multiple times');
index = i;
const middleware = middlewares[i];
if (!middleware) return next();
try {
middleware(req, res, () => dispatch(i + 1));
} catch (err) {
next(err);
}
}
dispatch(0);
};
}
const protected = composeMiddleware(
authenticate,
authorize('admin'),
rateLimit(100),
validateBody(schema)
);
app.post('/api/users', protected, createUser);
实战案例 2:数据转换管道
const transformUserData = pipe(
tap(data => console.log(`Raw records: ${data.length}`)),
filter(record => record.email && record.name),
map(pipe(
record => ({ ...record, email: record.email.toLowerCase().trim() }),
record => ({ ...record, name: record.name.trim() }),
record => ({ ...record, createdAt: new Date(record.createdAt) })
)),
records => [...new Map(records.map(r => [r.email, r])).values()],
sort((a, b) => b.createdAt - a.createdAt),
tap(data => console.log(`Clean records: ${data.length}`))
);
实战案例 3:配置化验证器
const isString = x => typeof x === 'string';
const isNumber = x => typeof x === 'number';
const minLength = min => s => s.length >= min;
const maxLength = max => s => s.length <= max;
const matches = regex => s => regex.test(s);
const inRange = (min, max) => n => n >= min && n <= max;
const validate = (rules) => (value) => {
const errors = [];
for (const [name, rule] of Object.entries(rules)) {
if (!rule(value)) errors.push(name);
}
return errors.length === 0 ? { valid: true } : { valid: false, errors };
};
const validateUsername = validate({
'Must be a string': isString,
'Min 3 characters': minLength(3),
'Max 20 characters': maxLength(20),
'Alphanumeric only': matches(/^[a-zA-Z0-9_]+$/)
});
console.log(validateUsername('alice_123'));
console.log(validateUsername('ab'));
9. 深度追问
Q1:函数组合的调试困难如何解决?
使用 tap 组合子在管道中插入日志。或使用带标签的 trace 函数:const trace = label => tap(x => console.log(label, x))。现代 DevTools 也支持在管道中设置条件断点。
Q2:柯里化函数的性能影响?
每次部分应用都创建新的闭包,有内存和调用开销。在热路径中应避免动态柯里化。可以使用编译时优化(如 Babel 插件)将柯里化调用展开为直接调用。
Q3:Transducers 何时优于链式调用?
当数据集大且变换步骤多时(避免 N 次中间数组创建),或需要跨集合类型复用变换逻辑时。对于小数组,链式 map().filter() 更可读,性能差异可忽略。
10. 总结表格
| 组合模式 | 描述 | 适用场景 |
|---|
| pipe | 从左到右组合 | 数据处理管道 |
| compose | 从右到左组合 | 数学风格组合 |
| curry | 逐参数应用 | 配置化函数 |
| partial | 固定部分参数 | 工厂函数 |
| transducer | 可组合的 reducer | 大数据集变换 |
| 组合子 | 签名 | 用途 |
|---|
| identity | a → a | 默认处理 |
| constant | a → () → a | 返回固定值 |
| tap | (a→void) → a → a | 副作用 |
| flip | (a,b→c) → (b,a→c) | 参数翻转 |
| not | (a→bool) → (a→bool) | 谓词取反 |