7

一文讲懂什么是函数柯里化,柯里化的目的及其代码实现

 1 year ago
source link: https://www.fly63.com/article/detial/12084
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

柯里化(Currying)

柯里化(Currying)是一种关于函数的高阶技术。它不仅被用于 JavaScript,还被用于其他编程语言。

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

柯里化不会调用函数。它只是对函数进行转换。

让我们先来看一个例子,以更好地理解我们正在讲的内容,然后再进行一个实际应用。

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

function curry(f) { // curry(f) 执行柯里化转换
return function(a) {
return function(b) {
return f(a, b);
};
};
}

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

let curriedSum = curry(sum);

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

正如你所看到的,实现非常简单:只有两个包装器(wrapper)。

  • curry(func) 的结果就是一个包装器 function(a) 。
  • 当它被像 curriedSum(1) 这样调用时,它的参数会被保存在词法环境中,然后返回一个新的包装器 function(b) 。
  • 然后这个包装器被以 2 为参数调用,并且,它将该调用传递给原始的 sum 函数。

柯里化更高级的实现,例如 lodash 库的 _.curry ,会返回一个包装器,该包装器允许函数被正常调用或者以偏函数(partial)的方式调用:

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

let curriedSum = _.curry(sum); // 使用来自 lodash 库的 _.curry

alert( curriedSum(1, 2) ); // 3,仍可正常调用
alert( curriedSum(1)(2) ); // 3,以偏函数的方式调用

柯里化?目的是什么?

要了解它的好处,我们需要一个实际中的例子。

例如,我们有一个用于格式化和输出信息的日志(logging)函数 log(date, importance, message) 。在实际项目中,此类函数具有很多有用的功能,例如通过网络发送日志(log),在这儿我们仅使用 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 会是带有固定第一个参数的日志的偏函数
let logNow = log(new Date());

// 使用它
logNow("INFO", "message"); // [HH:mm] INFO message

现在, logNow 是具有固定第一个参数的 log ,换句话说,就是更简短的“偏应用函数(partially applied function)”或“偏函数(partial)”。

我们可以更进一步,为当前的调试日志(debug 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,仍然可以被正常调用
alert( curriedSum(1)(2,3) ); // 6,对第一个参数的柯里化
alert( curriedSum(1)(2)(3) ); // 6,全柯里化

新的 curry 可能看上去有点复杂,但是它很容易理解。

curry(func) 调用的结果是如下所示的包装器 curried :

// func 是要转换的函数
function curried(...args) {
if (args.length >= func.length) { // (1)
return func.apply(this, args);
} else {
return function pass(...args2) { // (2)
return curried.apply(this, args.concat(args2));
}
}
};

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

  1. 现在调用:如果传入的 args 长度与原始函数所定义的( func.length )相同或者更长,那么只需要将调用传递给它即可。
  2. func
    pass
    curried
    

例如,让我们看看 sum(a, b, c) 这个例子。它有三个参数,所以 sum.length = 3 。

对于调用 curried(1)(2)(3) :

  1. curried(1)
    1
    pass
    
  2. pass
    (2)
    (1)
    (2)
    curried(1, 2)
    curry
    pass
    
  3. pass
    (3)
    pass(3)
    1
    2
    3
    curried(1, 2, 3)
    3
    

如果这还不够清楚,那你可以把函数调用顺序在你的脑海中或者在纸上过一遍。

只允许确定参数长度的函数

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

使用 rest 参数的函数,例如 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) )时,它会返回偏函数。

现代 JavaScript 教程:开源的现代 JavaScript 从入门到进阶的优质教程。 React 官方文档推荐,与 MDN 并列的 JavaScript 学习教程

在线免费阅读:https://zh.javascript.info

以上文章来源于技术漫谈 ,作者现代前端教程

链接: https://www.fly63.com/article/detial/12084


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK