Skip to content

柯里化 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 的 computedwatch 等 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); // 30

2. 提前返回

可以在参数不完整时返回一个函数,等待后续参数:

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 = 8

5. 提高代码可读性

通过语义化的函数名,使代码更易理解:

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 => {
  // ...
};

使用建议与最佳实践

什么时候用?

  1. 参数复用场景:需要基于同一配置创建多个函数
  2. 函数组合场景:需要将多个函数组合成管道
  3. 配置预设场景:需要预设部分参数,后续灵活调用

什么时候不用?

  1. 简单函数:参数少、逻辑简单的函数不需要柯里化
  2. 性能敏感场景:高频调用的函数避免过度柯里化
  3. 团队不熟悉:团队对函数式编程不熟悉时谨慎使用

实践原则

  1. 适度使用:不要为了柯里化而柯里化,只在真正需要参数复用或函数组合时使用
  2. 控制深度:避免过深的柯里化(建议不超过 3-4 层),超过这个深度通常意味着设计有问题
  3. 文档注释:对复杂的柯里化函数添加清晰的注释,说明参数顺序和用途
  4. 性能考虑:在性能敏感的场景中谨慎使用,必要时进行性能测试
  5. 团队协作:确保团队成员理解柯里化的概念,避免过度使用导致代码难以维护
  6. TypeScript 支持:在 TypeScript 项目中,合理使用类型推断,避免过度复杂的类型定义

总结

"总结一下,柯里化是一个强大的函数式编程技术,在参数复用、函数组合等场景下很有价值。但作为有经验的开发者,我认为技术选型要服务于业务需求,不要为了炫技而使用。 在实际项目中,我会在以下情况使用柯里化:API 封装、工具函数复用、配置预设等场景。同时要注意控制复杂度,确保代码可维护性。"

在 MIT 许可下发布