您现在的位置是:网站首页 > 柯里化模式(Currying)的函数式编程应用文章详情
柯里化模式(Currying)的函数式编程应用
陈川
【
JavaScript
】
3164人已围观
8509字
柯里化模式的概念
柯里化(Currying)是一种将多参数函数转换为一系列单参数函数的技术。它得名于数学家Haskell Curry,是函数式编程中的重要概念。柯里化后的函数每次只接受一个参数,并返回一个新函数来接收下一个参数,直到所有参数都被收集完毕才执行最终计算。
// 普通函数
function add(a, b, c) {
return a + b + c;
}
// 柯里化版本
function curriedAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
}
}
}
console.log(add(1, 2, 3)); // 6
console.log(curriedAdd(1)(2)(3)); // 6
JavaScript中的柯里化实现
在JavaScript中实现柯里化有多种方式,下面介绍几种常见方法:
手动柯里化
function multiply(a) {
return function(b) {
return function(c) {
return a * b * c;
}
}
}
const double = multiply(2);
const triple = double(3);
console.log(triple(4)); // 24
通用柯里化函数
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
}
}
}
}
const curriedSum = curry((a, b, c) => a + b + c);
console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2, 3)); // 6
ES6箭头函数实现
const curry = fn =>
judge = (...args) =>
args.length >= fn.length
? fn(...args)
: (...arg) => judge(...args, ...arg);
const add = curry((a, b, c) => a + b + c);
console.log(add(1)(2)(3)); // 6
柯里化的实际应用场景
参数复用
// 创建URL
function createURL(base, path, query) {
return `${base}/${path}?${query}`;
}
// 柯里化版本
function createURLCurried(base) {
return function(path) {
return function(query) {
return `${base}/${path}?${query}`;
}
}
}
const createGithubURL = createURLCurried('https://github.com');
const createRepoURL = createGithubURL('username/repo');
console.log(createRepoURL('tab=repositories'));
// https://github.com/username/repo?tab=repositories
延迟执行
// 事件处理
const handleClick = id => event => {
console.log(`Clicked element with id: ${id}`);
console.log('Event:', event);
};
document.getElementById('btn').addEventListener(
'click',
handleClick('my-button')
);
函数组合
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
const toUpperCase = str => str.toUpperCase();
const exclaim = str => `${str}!`;
const repeat = str => `${str} ${str}`;
const transform = compose(repeat, exclaim, toUpperCase);
console.log(transform('hello')); // HELLO! HELLO!
高级柯里化技巧
占位符功能
function curryWithPlaceholder(fn) {
return function curried(...args) {
// 过滤占位符
const actualArgs = args.slice(0, fn.length);
const hasPlaceholder = actualArgs.includes(curryWithPlaceholder.placeholder);
if (actualArgs.length >= fn.length && !hasPlaceholder) {
return fn.apply(this, actualArgs);
} else {
return function(...args2) {
// 替换占位符
const mergedArgs = args.map(arg =>
arg === curryWithPlaceholder.placeholder && args2.length
? args2.shift()
: arg
).concat(args2);
return curried.apply(this, mergedArgs);
}
}
}
}
curryWithPlaceholder.placeholder = Symbol();
const _ = curryWithPlaceholder.placeholder;
const fn = curryWithPlaceholder((a, b, c, d) => [a, b, c, d]);
console.log(fn(1)(2)(3)(4)); // [1, 2, 3, 4]
console.log(fn(1, _, 3)(2)(4)); // [1, 2, 3, 4]
console.log(fn(1, _, _, 4)(2)(3)); // [1, 2, 3, 4]
自动柯里化
function autoCurry(fn, arity = fn.length) {
return function curried(...args) {
if (args.length >= arity) {
return fn(...args);
}
return autoCurry(fn.bind(null, ...args), arity - args.length);
}
}
const add = autoCurry((a, b, c) => a + b + c);
console.log(add(1)(2)(3)); // 6
console.log(add(1, 2)(3)); // 6
console.log(add(1)(2, 3)); // 6
console.log(add(1, 2, 3)); // 6
柯里化与部分应用的比较
柯里化和部分应用(Partial Application)经常被混淆,但它们有本质区别:
// 部分应用
function partial(fn, ...presetArgs) {
return function(...laterArgs) {
return fn(...presetArgs, ...laterArgs);
}
}
// 柯里化
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
}
}
}
}
// 使用示例
function greet(greeting, name, punctuation) {
return `${greeting}, ${name}${punctuation}`;
}
// 部分应用
const sayHello = partial(greet, 'Hello');
console.log(sayHello('World', '!')); // "Hello, World!"
// 柯里化
const curriedGreet = curry(greet);
console.log(curriedGreet('Hello')('World')('!')); // "Hello, World!"
性能考量与优化
柯里化会带来一定的性能开销,主要体现在:
- 每次柯里化调用都会创建新的闭包
- 参数需要多次传递
- 调用栈深度增加
优化策略:
// 1. 避免过度柯里化
// 不好的做法:所有函数都柯里化
// 好的做法:只在需要时柯里化
// 2. 使用惰性柯里化
function lazyCurry(fn) {
let args = [];
return function curried(...newArgs) {
args = args.concat(newArgs);
if (args.length >= fn.length) {
const result = fn(...args);
args = [];
return result;
}
return curried;
}
}
// 3. 使用记忆化
function memoizedCurry(fn) {
const cache = new Map();
return function curried(...args) {
const key = args.join(',');
if (cache.has(key)) {
return cache.get(key);
}
if (args.length >= fn.length) {
const result = fn(...args);
cache.set(key, result);
return result;
} else {
const next = (...args2) => curried(...args, ...args2);
cache.set(key, next);
return next;
}
}
}
柯里化在函数组合中的应用
函数组合是函数式编程的核心概念,柯里化使其更加灵活:
// 工具函数
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const map = f => arr => arr.map(f);
const filter = f => arr => arr.filter(f);
const reduce = (f, init) => arr => arr.reduce(f, init);
// 柯里化数据处理管道
const processData = pipe(
filter(x => x.age > 18),
map(x => ({ ...x, name: x.name.toUpperCase() })),
reduce((acc, curr) => acc + curr.score, 0)
);
const data = [
{ name: 'Alice', age: 20, score: 85 },
{ name: 'Bob', age: 17, score: 65 },
{ name: 'Charlie', age: 22, score: 90 }
];
console.log(processData(data)); // 175 (85 + 90)
柯里化与React组件
在React中,柯里化可以用于创建高阶组件或处理事件:
// 柯里化事件处理器
const withLogging = (message) => (handler) => (event) => {
console.log(message);
handler(event);
};
const Button = ({ onClick, children }) => {
const handleClick = withLogging('Button clicked')(onClick);
return (
<button onClick={handleClick}>
{children}
</button>
);
};
// 使用
const App = () => {
const handleClick = () => {
console.log('Original click handler');
};
return (
<Button onClick={handleClick}>
Click Me
</Button>
);
};
柯里化与Redux
在Redux中,柯里化常用于action创建函数和中间件:
// 柯里化action创建函数
const createAction = type => payload => ({ type, payload });
const addTodo = createAction('ADD_TODO');
const removeTodo = createAction('REMOVE_TODO');
console.log(addTodo('Learn currying'));
// { type: 'ADD_TODO', payload: 'Learn currying' }
// 柯里化中间件
const loggerMiddleware = store => next => action => {
console.log('Dispatching:', action);
const result = next(action);
console.log('Next state:', store.getState());
return result;
};
// 柯里化reducer
const createReducer = (initialState, handlers) =>
(state = initialState, action) =>
handlers[action.type]
? handlers[action.type](state, action)
: state;
const todoReducer = createReducer([], {
'ADD_TODO': (state, action) => [...state, action.payload],
'REMOVE_TODO': (state, action) =>
state.filter(todo => todo !== action.payload)
});
柯里化的数学基础
柯里化源于数学中的柯里-霍华德同构(Curry-Howard correspondence),它展示了计算机程序与数学证明之间的深刻联系。在λ演算中,所有函数都只有一个参数,多参数函数通过柯里化实现:
λx.λy.λz.x + y + z
这对应于JavaScript中的:
const lambda = x => y => z => x + y + z;
柯里化的类型签名
在类型系统中,柯里化的类型转换可以表示为:
(a × b × c → d) → (a → b → c → d)
在TypeScript中:
type Curried<T, R> =
T extends [infer A, ...infer B]
? (arg: A) => Curried<B, R>
: R;
function curry<T extends any[], R>(
fn: (...args: T) => R
): Curried<T, R> {
return function curried(...args: any[]): any {
if (args.length >= fn.length) {
return fn(...args as any);
} else {
return (...moreArgs: any[]) => curried(...args, ...moreArgs);
}
} as any;
}
const add = (a: number, b: number, c: number): number => a + b + c;
const curriedAdd = curry(add);
const result = curriedAdd(1)(2)(3); // 6
上一篇: 异步等待模式(Async/Await)的同步风格编程
下一篇: 前端代码层面的防护措施