柯里化是函数式编程的两个最基本的运算之一
柯里化
要想让一个 f(x) 和一个 g(x) 合成为 f(g(x)),有一个隐藏的前提,就是 f 和 g 都只能接受一个参数。如果可以接受多个参数,比如 f(x, y) 和 g(a, b, c),函数合成就非常麻烦。
这时就需要函数柯里化了。所谓”柯里化”,就是把一个多参数的函数,转化为单参数函数。
例子:
// 柯里化之前
function add(x, y) {
return x + y;
}
add(1, 2) // 3
// 柯里化之后
function addX(y) {
return function (x) {
return x + y;
};
}
addX(2)(1) // 3
//简化 addX(y)
addX(y) = x => y =>x+y
有了柯里化以后,我们就能做到,所有函数只接受一个参数。后文的内容除非另有说明,都默认函数只有一个参数,就是所要处理的那个值。
如果我们想实现任意柯里化如:
function sum(a,b,c,d){
return a+b+c+d
}
let sum2 = curry(sum)
let sum3 = curry(sum)
sum2(1)(2)(3,4) //10
sum3(1)(2,3)(4) //10
curry分析
那我们要如何实现这个柯里化(curry)呢,先来分析一下,以 sum2 为例子。
首先 sum2 是一个函数,经过 curry 处理之后,得到另外一个结果,这个结果后面可以加括号,表示这个东西也是一个函数。这也是curry的雏形。
function curry(fn){
return function(){
return fn()
}
return function
}
}
接着往下分析,它接受一个函数,后面还能加括号,那就是它会返回另外一个函数,那么在这个例子中 sum2 = function,
如果我们的参数很多,是不是要嵌套很多很多层?涉及到这种不知道要嵌套多少层,不知道有多深的,我们就可以使用递归。
curry实现代码如下:
function curry(fn){
let arr = []
return function curried(...args){//...表示展开数组
arr = [...arr,...args]
if(arr.length >= fn.length){ //fn.length 即需要的参数的数量
return fn(...arr)
}
return curried
}
}
理解递归最好的办法就是代入法,尝试着带入sum2 = function,并逐个传入参数。
//开始
sum2 = function curried(...args){
arr = [...arr,...args]
if(arr.length >= fn.length){
return fn(...arr)
}
return curried
}
递归过程
//以sum2(1)(2)(3,4) 为例
arr = [] //是一个空数组
sum2(1) //此时,args = [1],arr=[1]
arr.length<fn.length
return curried
sum2(1)(2) //此时,args = [2],arr = [1,2]
arr.length<fn.length
return curried
sum2(1)(2)(3,4)//此时,args=[3,4],arr = [1,2,3,4]
arr.length >= fn.length
return fn(...arr) //递归结束,返回sum(1,2,3,4)
代码实践
既然 curry 已经写出来了,我们就检验一下它是否能够成功达到目的。
function sum(a,b,c,d){
return a+b+c+d
}
function curry(fn){
let arr = []
return function curried(...args){//...表示展开数组
arr = [...arr,...args]
if(arr.length >= fn.length){ //fn.length 即需要的参数的数量
return fn(...arr)
}
return curried
}
}
let sum2 = curry(sum)
sum2(1)(2)(3,4) //10
但是调试的时候马上会发现一个 bug,我如果想再次使用 sum2 算一个别的值呢?
sum2(4)(3)(9,6)
浏览器马上就会报错,说sum2不是一个函数,我不是明明已经声明了吗???
再次声明一边sum2。
let sum2 = curry(sum)
sum2(4)(3)(9,6)
这一次就成功了
再来一次
实例证明,在这段代码中,每一次使用 sum2 时,我们都必须再次声明,那不是太蠢了?我们再想想,如果 sum2 不是一个函数了,那么它到底是什么呢?
为了结果更直观,在每一次递归的时候打印出 arr 的值。
function curry(fn){
let arr = []
return function curried(...args){//...表示展开数组
arr = [...arr,...args]
console.log(arr) //打印数组的值
if(arr.length >= fn.length){ //fn.length 即需要的参数的数量
return fn(...arr)
}
return curried
}
}
再次调用 sum2
可以发现,如果不重新声明的话,新的参数会加在原有的数组里,且第一个括号内有什么,数组里就会增加什么。
回顾代码,由于 arr.length 已经大于 fn.length,所以函数都会直接 return fn(…arr),而不会再次 return curried。所以,如果不再次声明 sum2,继续调用 sum2(),例如 sum2(1),得到的结果就会是一个 fn 函数的调用,也就是sum 函数之前计算出来的一个 Number 。
修改代码
function sum(a,b,c,d){
return a+b+c+d
}
function curry(fn){
let arr = []
return function curried(...args){//...表示展开数组
arr = [...arr,...args]
console.log(arr)
if(arr.length >= fn.length){ //fn.length 即需要的参数的数量
let result = fn(...arr) //用另一个变量保存sum的值
arr.length = 0 //初始化数组
return result
}
return curried
}
}
let sum2 = curry(sum)
修改之后就可以正常使用了。
总结
- 所谓”柯里化”,就是把一个多参数的函数,拆解转化为单参数函数。
- curry的核心原理:闭包。不断引用一开始保存参数的列表。
- curry 的递归思想就是判断是否符合参数数量,不符合就继续 return curried。