当前位置: 首页 > 科技观察

JS中的柯里化和精巧的自动柯里化实现

时间:2023-03-19 01:33:24 科技观察

什么是柯里化?在计算机科学中,柯里化是将一个接受多个参数的函数转化为一个接受单个参数(原函数的第一个参数)的函数,并返回一个接受剩余参数并返回一个结果的新函数的技术。该技术由ChristopherStrachey以逻辑学家HaskellCurry的名字命名,尽管它是由MosesSchnfinkel和GottlobFrege发明的。理论看起来很大?没关系,我们先看代码:Curry应用假设我们需要实现一个函数,对列表元素进行一些处理,比如给列表中的每个元素加一个,那么很容易想到: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);//=>6plus2(7);//=>9把这样一个函数传入map:constlist=[0,1,2,3];list.map(plus1);//=>[1,2,3,4]list.map(plus2);//=>[2,3,4,5]是不是很Awesome?这样不管加多少,只需要list就可以了。map(plus(x)),终于实现了封装,可读性大大提高!(☆???)但是问题来了:如何实现这样的加号功能呢?这时候柯里化就可以派上用场了:柯里化函数//原始加法函数}}//ES6写法constplus=a=>b=>a+b;可以看出,curriedplus函数首先接受一个参数a,然后返回一个接受参数b的函数,由于闭包,返回的函数可以访问父函数的参数a,例如:constplus2=plus(2)可以等价地看作functionplus2(b){return2+b;},这样就完成了部分配置。通俗地说,柯里化是部分配置多参数函数的过程,每一步返回一个接受单个参数的部分配置函数。在一些极端的情况下,可能需要多次配置一个函数,比如多次添加:multiPlus(1)(2)(3);//=>6这个写法看起来很奇怪吧?但是如果进入JS函数式编程的大坑,这会是常态。(笑)JS中自动柯里化的复杂实现柯里化(Currying)是函数式编程中非常重要的一部分。许多函数式语言(例如Haskell)默认自动柯里化函数。但是JS并没有这样做,所以我们需要自己实现自动柯里化的功能。第一段代码://ES5functioncurry(fn){function_c(restNum,argsList){returnrestNum===0?fn.apply(null,argsList):function(x){return_c(restNum-1,argsList.concat(x));};}return_c(fn.length,[]);}//ES6constcurry=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;});//ES6constplus=curry((a,b)=>a+b);plus(2)(4);//=>6这样就实现了自动柯里化!(╭ ̄3 ̄)╭?如果你能明白发生了什么,那么恭喜你!你就是大家口中的大boss!╰(°▽°)╯,点个赞开始你的函数式生涯吧(搞笑不懂是怎么回事,别着急,我现在帮你理清思路,我们需要咖喱需求分析函数,接受一个待柯里化的函数作为参数,返回一个接收参数的函数,并将接收到的参数放入一个列表中,当参数个数足够时,执行原函数并返回结果。实现方法很简单想一想我们可以知道,curried部分配置函数的步数等于fn的参数个数,也就是说,有两个参数的plus函数需要分两个部分配置steps.函数的参数个数可以通过fn.length获取。大致的思路是每次传入参数时,将参数放入一个参数列表argsList中。如果没有更多参数需要传递,则调用fn.apply(null,argsList)执行原函数。为此,我们需要一个内部判断函数_c(restNum,argsList),该函数接受两个参数,一个是剩余参数的个数restNum,另一个是获取到的参数的列表argsList;_c的作用是判断是否还有未传入的参数。当restNum为0时,是时候通过fn.apply(null,argsList)执行原函数并返回结果。如果还有参数需要传递,即restNum不为0时,需要返回一个单参数函数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的写法看起来精简了很多,但是思路是一样的~//ES6constcurry=fn=>{const_c=(restNum,argsList)=>restNum===0?fn(...argsList):x=>_c(restNum-1,[...argsList,x]);return_c(fn.length,[]);}比较withothermethods也是有一个大家常用的方法:...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("咖喱");在我的电脑上(ManjaroLinux,IntelXeonE52665,32GBDDR3四通道1333Mhz,Node.js9.2.0):到达的方法大约需要0.325ms,其他方法大约需要0.345ms。可怜的猜测是关闭的原因。因为闭包的访问比较消耗性能,所以这个方法形成了两个闭包:fn和len。上面说的方法只是形成了一个fn的闭包,所以造成了这个小间隙。也希望大家可以自己测试一下,谈谈自己的看法~