柯里化 Curry
基础理解
柯里化(Currying)是以逻辑学家**哈斯凯尔·加里(Haskell Curry)**的名字命名的技术 通过闭包机制将多参数函数转换为单参数函数链,比如 add(a, b, c) 可以变成 add(a)(b)(c) 的形式 它将接受多个参数的函数转换为一系列每次接受一个参数的函数
本质:利用闭包保存参数,当参数数量足够时执行原函数
核心作用:参数保存复用、提前返回、延迟计算
通用柯里化实现
js
// 通用柯里化函数
const curry = (fn) => {
return function curried(...args) {
// 参数数量足够,执行原函数
if (args.length >= fn.length) {
return fn.apply(this, args);
}
// 参数不足,返回新函数继续收集参数
return function (...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
};
};
// 使用示例
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
curriedAdd(1)(2)(3); // 6
curriedAdd(1, 2)(3); // 6
curriedAdd(1)(2, 3); // 6源码级理解
React
React 的事件处理中,bind 方法就是柯里化的应用:
js
class Button extends React.Component {
handleClick = (id) => (e) => {
console.log('Button', id, 'clicked');
};
render() {
return <button onClick={this.handleClick(this.props.id)}>Click</button>;
}
}Vue 3 Composition API
Vue 3 的 computed、watch 等 API 也体现了柯里化思想:
js
// 部分应用参数,返回配置好的函数
const useCounter = (initialValue) => {
const count = ref(initialValue);
const increment = () => count.value++;
return { count, increment };
};实战经验
1. API 请求封装
js
// 创建通用的请求函数
const request = (baseURL) => (method) => (endpoint) => (data) => {
return fetch(`${baseURL}${endpoint}`, {
method,
body: JSON.stringify(data),
});
};
// 预设 baseURL
const apiRequest = request('https://api.example.com');
// 预设 method
const get = apiRequest('GET');
const post = apiRequest('POST');
// 使用
const getUser = get('/user');
const createUser = post('/user');2. 事件处理函数
js
// 通用的事件处理器
const handleEvent = (eventType) => (handler) => (e) => {
if (e.type === eventType) {
handler(e);
}
};
// 预设事件类型
const handleClick = handleEvent('click');
const handleInput = handleEvent('input');
// 使用
const onClick = handleClick((e) => console.log('clicked'));
const onInput = handleInput((e) => console.log('input:', e.target.value));3. 数据验证
js
const createValidator = (rule) => (message) => (value) => {
if (!rule.test(value)) {
throw new Error(message);
}
return true;
};
// 预设常用验证器
const validateEmail = createValidator(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)('邮箱格式不正确');
const validatePhone = createValidator(/^1[3-9]\d{9}$/)('手机号格式不正确');
// 在表单中使用
const form = {
email: validateEmail,
phone: validatePhone,
};4. 日志
js
// 日志函数
const log = (level) => (message) => {
console.log(`[${level}] ${new Date().toISOString()} - ${message}`);
};
const info = log('INFO');
const error = log('ERROR');
const warn = log('WARN');
// 使用
info('用户登录成功');
error('数据库连接失败');5. 工具
js
// 类型判断 - 参数复用
const isType = (type) => (target) =>
`[object ${type}]` === Object.prototype.toString.call(target);
const isArray = isType('Array');
const isObject = isType('Object');
isArray([1, 2]); // true
isObject({}); // true深度思考与权衡
"经过多年实践,我认为柯里化有以下价值,但也需要注意其局限性:"
柯里化的好处
1. 参数复用
通过固定部分参数,生成新的函数,减少重复代码:
js
const add = (a) => (b) => a + b;
const add10 = add(10);
add10(5); // 15
add10(20); // 302. 提前返回
可以在参数不完整时返回一个函数,等待后续参数:
js
const match = (reg) => (str) => reg.test(str);
const hasNumber = match(/\d+/);
const hasLetter = match(/[a-zA-Z]+/);3. 延迟计算
只有在所有参数都传入时才执行计算,可以用于性能优化:
js
const expensiveOperation = (a) => (b) => (c) => {
// 只有在所有参数都传入时才执行
return a * b * c;
};4. 函数组合
柯里化使得函数组合更加容易,便于构建复杂的功能:
js
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);
const add = (a) => (b) => a + b;
const multiply = (a) => (b) => a * b;
const addThenMultiply = pipe(add(1), multiply(2));
addThenMultiply(3); // (3 + 1) * 2 = 85. 提高代码可读性
通过语义化的函数名,使代码更易理解:
js
const filter = (predicate) => (arr) => arr.filter(predicate);
const map = (fn) => (arr) => arr.map(fn);
const getEvenNumbers = filter((n) => n % 2 === 0);
const doubleNumbers = map((n) => n * 2);柯里化带来的问题
1. 可读性降低
过度使用柯里化可能导致代码难以理解,特别是对于不熟悉函数式编程的开发者:
js
// 过度柯里化,难以理解
const fn = (a) => (b) => (c) => (d) => (e) => a + b + c + d + e;2. 性能开销
柯里化会创建多层嵌套函数,每次调用都会创建新的函数闭包,可能带来性能开销:
js
// 每次调用都会创建新的函数
const add = (a) => (b) => a + b;
const add1 = add(1); // 创建闭包
const add2 = add(2); // 创建闭包3. 调试困难
多层嵌套的函数调用栈,使得调试变得困难:
js
// 调用栈会更深
const result = fn(1)(2)(3)(4)(5);
// 错误堆栈会显示多层嵌套函数4. 参数顺序限制
柯里化要求参数按固定顺序传入,不够灵活:
js
// 必须按顺序传入参数
const divide = (a) => (b) => a / b;
divide(10)(2); // 5
// 如果想先传入除数,需要重新定义
const divideBy = (b) => (a) => a / b;5. 内存占用
每个柯里化函数都会保存闭包,可能增加内存占用:
js
// 每个部分应用的函数都会保存闭包
const createFunctions = () => {
const functions = [];
for (let i = 0; i < 1000; i++) {
functions.push((x) => (y) => x + y + i);
}
return functions;
};6. 类型推断困难
在 TypeScript 中,深度柯里化的类型推断可能变得复杂:
typescript
// 类型推断可能变得复杂
const curry = <T extends any[], R>(
fn: (...args: T) => R
): T extends [infer First, ...infer Rest]
? (arg: First) => Rest extends [] ? R : ReturnType<typeof curry<Rest, R>>
: R => {
// ...
};使用建议与最佳实践
什么时候用?
- 参数复用场景:需要基于同一配置创建多个函数
- 函数组合场景:需要将多个函数组合成管道
- 配置预设场景:需要预设部分参数,后续灵活调用
什么时候不用?
- 简单函数:参数少、逻辑简单的函数不需要柯里化
- 性能敏感场景:高频调用的函数避免过度柯里化
- 团队不熟悉:团队对函数式编程不熟悉时谨慎使用
实践原则
- 适度使用:不要为了柯里化而柯里化,只在真正需要参数复用或函数组合时使用
- 控制深度:避免过深的柯里化(建议不超过 3-4 层),超过这个深度通常意味着设计有问题
- 文档注释:对复杂的柯里化函数添加清晰的注释,说明参数顺序和用途
- 性能考虑:在性能敏感的场景中谨慎使用,必要时进行性能测试
- 团队协作:确保团队成员理解柯里化的概念,避免过度使用导致代码难以维护
- TypeScript 支持:在 TypeScript 项目中,合理使用类型推断,避免过度复杂的类型定义
总结
"总结一下,柯里化是一个强大的函数式编程技术,在参数复用、函数组合等场景下很有价值。但作为有经验的开发者,我认为技术选型要服务于业务需求,不要为了炫技而使用。 在实际项目中,我会在以下情况使用柯里化:API 封装、工具函数复用、配置预设等场景。同时要注意控制复杂度,确保代码可维护性。"