【JavaScript】函数式编程:一文搞懂函数科里化(Currying)~
在之前的两篇文章:
我介绍了JS函数式编程中的一些概念及其实践,本文则着重详细、深入介绍里面最常用的技术之一 —— 函数柯里化。
柯里化(Currying)
柯里化(Currying)是一种关于函数的高阶技术,它允许你将一个有多个参数的函数转化为一连串的函数,它返回一个新的函数,等待下一个参数的内联。换句话说,一个函数不是一次接受所有的参数,而是接受第一个参数并返回一个新的函数,该函数接受第二个参数并返回一个新的函数,该函数接受第三个参数,以此类推,直到所有参数都得到满足。
这个概念是以数学家
Haskell Curry
的名字命名的,他在1930年代提出了这个概念。在JavaScript中,Currying
是一个强大的工具,它可以将一个有多个参数的函数转化为一系列嵌套的函数,每个函数只接受一个参数。Currying
帮助你避免多次传递同一个变量,并帮助你创建一个高阶函数。可以提高代码的可重用性、可组合性和可维护性。
在本文中,我们将深入探讨curry,提供全面的解释和例子。
柯里化有两个特点:
- 柯里化是一种函数的转换,它是指将一个函数从可调用的
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,以部分应用函数的方式调用
函数科里化(Currying)是JavaScript中的一个高级特性,它能够将接受多个参数的函数变成一系列只接受部分参数的函数。这种技术在函数式编程中经常使用,并且可以使代码更加灵活和可复用。以下是一个详细的js代码示例,展示了如何实现函数科里化。
// 定义一个普通的函数,它接收两个参数并返回他们的和
function add(x, y) {
return x + y;
}
// 编写一个curry函数,它可以对任何函数进行科里化
function curry(fn) {
// 获取除第一个参数外的其他参数
var args = Array.prototype.slice.call(arguments, 1);
// 返回一个新函数,如果参数足够执行则调用原来的函数,否则继续科里化
return function() {
var newArgs = args.concat(Array.prototype.slice.call(arguments));
if (newArgs.length >= fn.length) {
return fn.apply(this, newArgs);
} else {
return curry.call(this, fn, ...newArgs);
}
};
}
// 使用curry函数对add进行科里化
var curriedAdd = curry(add);
// 调用curriedAdd函数,返回一个新函数
var add1 = curriedAdd(1);
// 继续调用新函数,返回结果2
console.log(add1(1)); // 2
// 另一种调用方式
console.log(curriedAdd(2)(3)); // 5
在上面的代码中,我们首先定义了一个普通的函数add
,它接收两个参数并返回他们的和。然后,我们编写了一个名为curry
的函数,它接受一个要进行科里化的函数作为第一个参数,并且通过Array.prototype.slice.call(arguments, 1)
获取除了第一个参数外的其他参数。
在curry
函数内部,我们返回了一个新函数,这个新函数如果接收到的参数足够执行,则调用原来的函数。否则,继续使用curry
函数对其进行科里化。这个过程通过newArgs
变量将之前传入的参数和当前传入的参数合并起来实现。最后,我们通过return curry.call(this, fn, ...newArgs);
递归调用curry
函数,直到参数足够执行时结束递归调用。
我们使用curry
函数对add
函数进行科里化,得到一个名为curriedAdd
的函数。我们可以使用curriedAdd
函数来创建新的函数,例如使用curriedAdd(1)
返回一个只需要一个参数的新函数。当我们调用这个新函数时,它会返回一个结果,而不是立即执行。当我们重新调用这个函数,将它的返回值和另一个参数一起传入时,它会再次返回结果,依此类推。
总之,函数科里化是JavaScript中一个非常强大的技术,它可以使代码更加灵活和可复用。在某些特定场景下,使用函数科里化可以简化代码并提高开发效率。
柯里化?目的是什么?
要了解它的好处,我们需要一个实际中的例子。
例如,我们有一个用于格式化和输出信息的日志(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
所以:
- 柯里化之后,我们没有丢失任何东西:
log
依然可以被正常调用。 - 我们可以轻松地生成部分应用函数,例如用于生成今天的日志的部分应用函数。
高级柯里化实现
如果你想了解更多细节,下面是用于多参数函数的“高级”柯里化实现,我们也可以把它用于上面的示例。
它非常短:
// js 版本
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));
}
}
};
}
// ts 版本
function curry<T extends (...args: any[]) => any>(func: T) {
return function curried(...args: Parameters<T>) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function(...args2: Parameters<T>) {
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(...args2) { // (2)
return curried.apply(this, args.concat(args2));
}
}
};
当我们运行它时,这里有两个 if
执行分支:
- 如果传入的
args
长度与原始函数所定义的(func.length
)相同或者更长,那么只需要使用func.apply
将调用传递给它即可。 - 否则,获取一个部分应用函数:我们目前还没调用
func
。取而代之的是,返回另一个包装器pass
,它将重新应用curried
,将之前传入的参数与新的参数一起传入。
然后,如果我们再次调用它,我们将得到一个新的部分应用函数(如果没有足够的参数),或者最终的结果。
只允许确定参数长度的函数
- 柯里化要求函数具有固定数量的参数。
- 使用 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)
)时,它会返回部分应用函数。
end~
转载自:https://juejin.cn/post/7241395652092821541