← 中文资源

简体中文版本

支持占位符的柯里化

把多参数函数转换为可分步传参的柯里化函数,支持占位符与后续参数填补。

阅读英文原文

功能说明

curry 把接收多个参数的函数转换为每次可传部分参数的版本,参数足够时才真正调用原函数。占位符常量允许先跳过某个位置,用后续调用的参数填补。uncurry 则把一串单参数调用还原为一次性传参。适合构造可复用的事件处理器、配置化转换函数、函数组合以及测试中的部分应用。

关键代码

柯里化依赖函数的 length 属性推断参数个数,也允许显式传入 arity 以处理默认参数或剩余参数。每次调用先截取所需参数数量,若已足够且不含占位符就通过 apply 保留 this 并执行原函数。否则返回收集器,把占位符按顺序替换为新参数,多余参数追加到末尾,再递归进入柯里化函数。占位符使用 Symbol,避免与普通参数冲突。

使用注意

函数的 length 不包含默认值和剩余参数,这类函数应明确传入 arity,否则可能过早执行或永远无法满足。占位符未被填补时会一直返回新函数,调用方应避免无限累积。每次部分应用都会创建新数组,极端热路径需评估分配开销。该方法不改变原函数,也不做参数类型校验,类型安全应交给 TypeScript 重载。

验证方式

测试一次传满参数、逐个传参、先传两个再传一个以及带占位符的乱序传参,断言结果一致。验证 this 绑定在方法和普通函数中的表现。测试 arity 为零、默认参数函数显式指定 arity、非法 arity 与非函数输入。再用 uncurry 还原柯里化函数,确认多参数调用得到相同结果。

代码示例

export const curryPlaceholder = Symbol("curry.placeholder");

export function curry(fn, arity = fn.length) {
  if (typeof fn !== "function") throw new TypeError("fn must be a function");
  if (!Number.isInteger(arity) || arity < 0) throw new RangeError("arity must be a non-negative integer");
  return function curried(...args) {
    const supplied = args.slice(0, arity);
    if (supplied.length === arity && !supplied.includes(curryPlaceholder)) {
      return fn.apply(this, supplied);
    }
    return function next(...more) {
      const merged = [];
      let cursor = 0;
      for (const argument of args) {
        if (argument === curryPlaceholder && cursor < more.length) {
          merged.push(more[cursor]);
          cursor += 1;
        } else {
          merged.push(argument);
        }
      }
      while (cursor < more.length) {
        merged.push(more[cursor]);
        cursor += 1;
      }
      return curried.apply(this, merged);
    };
  };
}

export function uncurry(curried) {
  if (typeof curried !== "function") throw new TypeError("curried must be a function");
  return (...args) => args.reduce((fn, argument) => fn(argument), curried);
}