2021 年 1 月 10 日

柯里化

柯里化是一种处理函数的高级技术。它不仅用于 JavaScript,还用于其他语言。

柯里化是一种函数转换,它将函数从可调用为 f(a, b, c) 的形式转换为可调用为 f(a)(b)(c) 的形式。

柯里化不会调用函数。它只会转换函数。

让我们先看一个示例,以便更好地理解我们在谈论什么,然后再看实际应用。

我们将创建一个辅助函数 curry(f),它对一个有两个参数的 f 执行柯里化。换句话说,curry(f) 对于有两个参数的 f(a, b),将其转换为一个以 f(a)(b) 形式运行的函数

function curry(f) { // curry(f) does the currying transform
  return function(a) {
    return function(b) {
      return f(a, b);
    };
  };
}

// usage
function sum(a, b) {
  return a + b;
}

let curriedSum = curry(sum);

alert( curriedSum(1)(2) ); // 3

如你所见,实现非常简单:它只是两个包装器。

  • curry(func) 的结果是一个包装器 function(a)
  • 当它被调用为 curriedSum(1) 时,参数保存在词法环境中,并返回一个新的包装器 function(b)
  • 然后,使用 2 作为参数调用此包装器,它将调用传递给原始的 sum

柯里化的更高级实现,例如来自 lodash 库的 _.curry,返回一个包装器,该包装器允许函数以正常和部分方式调用

function sum(a, b) {
  return a + b;
}

let curriedSum = _.curry(sum); // using _.curry from lodash library

alert( curriedSum(1, 2) ); // 3, still callable normally
alert( curriedSum(1)(2) ); // 3, called partially

柯里化?为什么?

要了解好处,我们需要一个有价值的真实示例。

例如,我们有日志记录函数 log(date, importance, message),它格式化并输出信息。在实际项目中,此类函数具有许多有用的特性,例如通过网络发送日志,这里我们只使用 alert

function log(date, importance, message) {
  alert(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`);
}

让我们对其进行柯里化!

log = _.curry(log);

之后 log 正常工作

log(new Date(), "DEBUG", "some debug"); // log(a, b, c)

…但也可以在柯里化形式中工作

log(new Date())("DEBUG")("some debug"); // log(a)(b)(c)

现在,我们可以轻松地为当前日志制作一个便捷函数

// logNow will be the partial of log with fixed first argument
let logNow = log(new Date());

// use it
logNow("INFO", "message"); // [HH:mm] INFO message

现在 logNow 是具有固定第一个参数的 log,换句话说,它是“部分应用函数”或简称“部分”。

我们可以更进一步,为当前调试日志制作一个便捷函数

let debugNow = logNow("DEBUG");

debugNow("message"); // [HH:mm] DEBUG message

所以

  1. 在柯里化之后,我们什么也没丢失:log 仍然可以正常调用。
  2. 我们可以轻松地生成部分函数,例如用于今天的日志。

高级柯里化实现

如果你想了解详细信息,这里是我们可以在上面使用的多参数函数的“高级”柯里化实现。

它很短

function curry(func) {

  return function curried(...args) {
    if (args.length >= func.length) {
      return func.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      }
    }
  };

}

用法示例

function sum(a, b, c) {
  return a + b + c;
}

let curriedSum = curry(sum);

alert( curriedSum(1, 2, 3) ); // 6, still callable normally
alert( curriedSum(1)(2,3) ); // 6, currying of 1st arg
alert( curriedSum(1)(2)(3) ); // 6, full currying

新的 curry 看起来可能很复杂,但实际上很容易理解。

curry(func) 调用的结果是包装器 curried,它看起来像这样

// func is the function to transform
function curried(...args) {
  if (args.length >= func.length) { // (1)
    return func.apply(this, args);
  } else {
    return function(...args2) { // (2)
      return curried.apply(this, args.concat(args2));
    }
  }
};

当我们运行它时,有两个 if 执行分支

  1. 如果传递的 args 计数与原始函数在其定义中拥有的计数相同或更多(func.length),则只需使用 func.apply 将调用传递给它。
  2. 否则,获取一个部分:我们现在不调用 func。相反,返回另一个包装器,它将重新应用 curried,提供以前的参数和新参数。

然后,如果我们再次调用它,我们将获得一个新的部分(如果没有足够的参数)或最终的结果。

仅固定长度函数

柯里化要求函数具有固定数量的参数。

使用剩余参数的函数(例如 f(...args))不能以这种方式进行柯里化。

比柯里化多一点

根据定义,柯里化应将 sum(a, b, c) 转换为 sum(a)(b)(c)

但 JavaScript 中的大多数柯里化实现都是高级的,如下所述:它们还使函数在多参数变体中可调用。

总结

柯里化是一种转换,它使 f(a,b,c) 可作为 f(a)(b)(c) 调用。JavaScript 实现通常既保持函数正常可调用,又当参数数量不足时返回部分。

柯里化使我们能够轻松地获得部分。正如我们在日志记录示例中看到的那样,在对三个参数通用函数 log(date, importance, message) 进行柯里化后,当用一个参数(如 log(date))或两个参数(如 log(date, importance))调用时,它会给我们部分。

教程地图

评论

在评论之前阅读此内容…
  • 如果您有改进建议 - 请 提交 GitHub 问题或提交拉取请求,而不是评论。
  • 如果您无法理解文章中的某些内容 - 请详细说明。
  • 要插入几行代码,请使用 <code> 标记,对于多行 - 将它们包装在 <pre> 标记中,对于超过 10 行 - 使用沙盒(plnkrjsbincodepen…)