JS中精巧的自动柯里化实现方法
以下内容通过代码讲解和实例分析了JS中精巧的自动柯里化实现方法,并分析了柯里化函数的基础用法和知识,学习一下吧。
什么是柯里化?
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由ChristopherStrachey以逻辑学家HaskellCurry命名的,尽管它是MosesSchnfinkel和GottlobFrege发明的。
理论看着头大?没关系,先看看代码:
柯里化应用
假设我们需要实现一个对列表元素进行某种处理的功能,比如说让列表内每一个元素加一,那么很容易想到:
constlist=[0,1,2,3]; list.map(elem=>elem+1);
很简单是吧?如果又要加2呢?
constlist=[0,1,2,3]; list.map(elem=>elem+1); list.map(elem=>elem+2);
看上去效率有点低,处理函数封装下?
可是map的回调函数只接受当前元素elem这一个参数,看上去好像没有办法封装...
你也许会想:如果能拿到一个部分配置好的函数就好了,比如说:
//plus返回部分配置好的函数 constplus1=plus(1); constplus2=plus(2); plus1(5);//=>6 plus2(7);//=>9
把这样的函数传进map:
constlist=[0,1,2,3]; list.map(plus1);//=>[1,2,3,4] list.map(plus2);//=>[2,3,4,5]
是不是很棒棒?这样一来不管是加多少,只需要list.map(plus(x))就好了,完美实现了封装,可读性大大提高!
不过问题来了:这样的plus函数要怎么实现呢?
这时候柯里化就能派上用场了:
柯里化函数
//原始的加法函数 functionorigPlus(a,b){ returna+b; } //柯里化后的plus函数 functionplus(a){ returnfunction(b){ returna+b; } } //ES6写法 constplus=a=>b=>a+b;
可以看到,柯里化的plus函数首先接受一个参数a,然后返回一个接受一个参数b的函数,由于闭包的原因,返回的函数可以访问到父函数的参数a,所以举个例子:constplus2=plus(2)就可等效视为functionplus2(b){return2+b;},这样就实现了部分配置。
通俗地讲,柯里化就是一个部分配置多参数函数的过程,每一步都返回一个接受单个参数的部分配置好的函数。一些极端的情况可能需要分很多次来部分配置一个函数,比如说多次相加:
multiPlus(1)(2)(3);//=>6
这种写法看着很奇怪吧?不过如果入了JS的函数式编程这个大坑的话,这会是常态。
JS中自动柯里化的精巧实现
柯里化(Currying)是函数式编程中很重要的一环,很多函数式语言(eg.Haskell)都会默认将函数自动柯里化。然而JS并不会这样,因此我们需要自己来实现自动柯里化的函数。
先上代码:
//ES5 functioncurry(fn){ function_c(restNum,argsList){ returnrestNum===0? fn.apply(null,argsList): function(x){ return_c(restNum-1,argsList.concat(x)); }; } return_c(fn.length,[]); } //ES6 constcurry=fn=>{ const_c=(restNum,argsList)=>restNum===0? fn(...argsList):x=>_c(restNum-1,[...argsList,x]); return_c(fn.length,[]); } /*****************使用*********************/ varplus=curry(function(a,b){ returna+b; }); //ES6 constplus=curry((a,b)=>a+b); plus(2)(4);//=>6
这样就实现了自动的柯里化!
如果你看得懂发生了什么的话,那么恭喜你!大家口中的大佬就是你!,快留下赞然后去开始你的函数式生涯吧(滑稽
如果你没看懂发生了什么,别担心,我现在开始帮你理一下思路。
需求分析
我们需要一个curry函数,它接受一个待柯里化的函数为参数,返回一个用于接收一个参数的函数,接收到的参数放到一个列表中,当参数数量足够时,执行原函数并返回结果。
实现方式
简单思考可以知道,柯里化部分配置函数的步骤数等于fn的参数个数,也就是说有两个参数的plus函数需要分两步来部分配置。函数的参数个数可以通过fn.length获取。
总的想法就是每传一次参,就把该参数放入一个参数列表argsList中,如果已经没有要传的参数了,那么就调用fn.apply(null,argsList)将原函数执行。要实现这点,我们就需要一个内部的判断函数_c(restNum,argsList),函数接受两个参数,一个是剩余参数个数restNum,另一个是已获取的参数的列表argsList;_c的功能就是判断是否还有未传入的参数,当restNum为零时,就是时候通过fn.apply(null,argsList)执行原函数并返回结果了。如果还有参数需要传递的话,也就是说restNum不为零时,就需要返回一个单参数函数
function(x){ return_c(restNum-1,argsList.concat(x)); }
来继续接收参数。这里形成了一个尾递归,函数接受了一个参数后,剩余需要参数数量restNum减一,并将新参数x加入argsList后传入_c进行递归调用。结果就是,当参数数量不足时,返回负责接收新参数的单参数函数,当参数够了时,就调用原函数并返回。
现在再来看:
functioncurry(fn){ function_c(restNum,argsList){ returnrestNum===0? fn.apply(null,argsList): function(x){ return_c(restNum-1,argsList.concat(x)); }; } return_c(fn.length,[]);//递归开始 }
是不是开始清晰起来了?
ES6写法的由于使用了数组解构及箭头函数等语法糖,看上去精简很多,不过思想都是一样的啦~
//ES6 constcurry=fn=>{ const_c=(restNum,argsList)=>restNum===0? fn(...argsList):x=>_c(restNum-1,[...argsList,x]); return_c(fn.length,[]); }
与其他方法的对比
还有一种大家常用的方法:
functioncurry(fn){ constlen=fn.length; returnfunctionjudge(...args1){ returnargs1.length>=len? fn(...args1): function(...args2){ returnjudge(...[...args1,...args2]); } } } //使用箭头函数 constcurry=fn=>{ constlen=fn.length; constjudge=(...args1)=>args1.length>=len? fn(...args1):(...args2)=>judge(...[...args1,...args2]); returnjudge; }
与本篇文章先前提到的方法对比的话,发现这种方法有两个问题:
依赖ES6的解构(函数参数中的...args1与...args2);
性能稍差一点。
性能问题
做个测试:
console.time("curry"); constplus=curry((a,b,c,d,e)=>a+b+c+d+e); plus(1)(2)(3)(4)(5); console.timeEnd("curry");
在我的电脑(ManjaroLinux,IntelXeonE52665,32GBDDR3四通道1333Mhz,Node.js9.2.0)上:
本篇提到的方法耗时约0.325ms
其他方法的耗时约0.345ms
差的这一点猜测是闭包的原因。由于闭包的访问比较耗性能,而这种方式形成了两个闭包:fn和len,前面提到的方法只形成了fn一个闭包,所以造成了这一微小的差距。