柯里化是函数式编程的两个最基本的运算之一

柯里化


要想让一个 f(x) 和一个 g(x) 合成为 f(g(x)),有一个隐藏的前提,就是 f 和 g 都只能接受一个参数。如果可以接受多个参数,比如 f(x, y) 和 g(a, b, c),函数合成就非常麻烦。

这时就需要函数柯里化了。所谓”柯里化”,就是把一个多参数的函数,转化为单参数函数。

例子:

  1. // 柯里化之前
  2. function add(x, y) {
  3. return x + y;
  4. }
  5. add(1, 2) // 3
  6. // 柯里化之后
  7. function addX(y) {
  8. return function (x) {
  9. return x + y;
  10. };
  11. }
  12. addX(2)(1) // 3
  13. //简化 addX(y)
  14. addX(y) = x => y =>x+y

有了柯里化以后,我们就能做到,所有函数只接受一个参数。后文的内容除非另有说明,都默认函数只有一个参数,就是所要处理的那个值。

如果我们想实现任意柯里化如:

  1. function sum(a,b,c,d){
  2. return a+b+c+d
  3. }
  4. let sum2 = curry(sum)
  5. let sum3 = curry(sum)
  6. sum2(1)(2)(3,4) //10
  7. sum3(1)(2,3)(4) //10

curry分析


那我们要如何实现这个柯里化(curry)呢,先来分析一下,以 sum2 为例子。

首先 sum2 是一个函数,经过 curry 处理之后,得到另外一个结果,这个结果后面可以加括号,表示这个东西也是一个函数。这也是curry的雏形。

  1. function curry(fn){
  2. return function(){
  3. return fn()
  4. }
  5. return function
  6. }
  7. }

接着往下分析,它接受一个函数,后面还能加括号,那就是它会返回另外一个函数,那么在这个例子中 sum2 = function,

如果我们的参数很多,是不是要嵌套很多很多层?涉及到这种不知道要嵌套多少层,不知道有多深的,我们就可以使用递归。

curry实现代码如下:

  1. function curry(fn){
  2. let arr = []
  3. return function curried(...args){//...表示展开数组
  4. arr = [...arr,...args]
  5. if(arr.length >= fn.length){ //fn.length 即需要的参数的数量
  6. return fn(...arr)
  7. }
  8. return curried
  9. }
  10. }

理解递归最好的办法就是代入法,尝试着带入sum2 = function,并逐个传入参数。

  1. //开始
  2. sum2 = function curried(...args){
  3. arr = [...arr,...args]
  4. if(arr.length >= fn.length){
  5. return fn(...arr)
  6. }
  7. return curried
  8. }

递归过程


  1. //以sum2(1)(2)(3,4) 为例
  2. arr = [] //是一个空数组
  3. sum2(1) //此时,args = [1],arr=[1]
  4. arr.length<fn.length
  5. return curried
  6. sum2(1)(2) //此时,args = [2],arr = [1,2]
  7. arr.length<fn.length
  8. return curried
  9. sum2(1)(2)(3,4)//此时,args=[3,4],arr = [1,2,3,4]
  10. arr.length >= fn.length
  11. return fn(...arr) //递归结束,返回sum(1,2,3,4)

代码实践

既然 curry 已经写出来了,我们就检验一下它是否能够成功达到目的。

  1. function sum(a,b,c,d){
  2. return a+b+c+d
  3. }
  4. function curry(fn){
  5. let arr = []
  6. return function curried(...args){//...表示展开数组
  7. arr = [...arr,...args]
  8. if(arr.length >= fn.length){ //fn.length 即需要的参数的数量
  9. return fn(...arr)
  10. }
  11. return curried
  12. }
  13. }
  14. let sum2 = curry(sum)
  15. sum2(1)(2)(3,4) //10

image.png

但是调试的时候马上会发现一个 bug,我如果想再次使用 sum2 算一个别的值呢?

  1. sum2(4)(3)(9,6)

image.png
浏览器马上就会报错,说sum2不是一个函数,我不是明明已经声明了吗???

再次声明一边sum2。

  1. let sum2 = curry(sum)
  2. sum2(4)(3)(9,6)

这一次就成功了
image.png
再来一次
image.png

实例证明,在这段代码中,每一次使用 sum2 时,我们都必须再次声明,那不是太蠢了?我们再想想,如果 sum2 不是一个函数了,那么它到底是什么呢?

为了结果更直观,在每一次递归的时候打印出 arr 的值。

  1. function curry(fn){
  2. let arr = []
  3. return function curried(...args){//...表示展开数组
  4. arr = [...arr,...args]
  5. console.log(arr) //打印数组的值
  6. if(arr.length >= fn.length){ //fn.length 即需要的参数的数量
  7. return fn(...arr)
  8. }
  9. return curried
  10. }
  11. }

再次调用 sum2
image.png
可以发现,如果不重新声明的话,新的参数会加在原有的数组里,且第一个括号内有什么,数组里就会增加什么。

回顾代码,由于 arr.length 已经大于 fn.length,所以函数都会直接 return fn(…arr),而不会再次 return curried。所以,如果不再次声明 sum2,继续调用 sum2(),例如 sum2(1),得到的结果就会是一个 fn 函数的调用,也就是sum 函数之前计算出来的一个 Number 。

修改代码


  1. function sum(a,b,c,d){
  2. return a+b+c+d
  3. }
  4. function curry(fn){
  5. let arr = []
  6. return function curried(...args){//...表示展开数组
  7. arr = [...arr,...args]
  8. console.log(arr)
  9. if(arr.length >= fn.length){ //fn.length 即需要的参数的数量
  10. let result = fn(...arr) //用另一个变量保存sum的值
  11. arr.length = 0 //初始化数组
  12. return result
  13. }
  14. return curried
  15. }
  16. }
  17. let sum2 = curry(sum)

修改之后就可以正常使用了。
image.png

总结


  1. 所谓”柯里化”,就是把一个多参数的函数,拆解转化为单参数函数。
  2. curry的核心原理:闭包。不断引用一开始保存参数的列表。
  3. curry 的递归思想就是判断是否符合参数数量,不符合就继续 return curried。