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

开发 前端
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。

[[212818]]

什么是柯里化?

在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的***个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses SchnfinkelGottlob Frege 发明的。

理论看着头大?没关系,先看看代码:

柯里化应用

假设我们需要实现一个对列表元素进行某种处理的功能,比如说让列表内每一个元素加一,那么很容易想到:

  1. const list = [0, 1, 2, 3]; 
  2. list.map(elem => elem + 1); 

很简单是吧?如果又要加2呢?

  1. const list = [0, 1, 2, 3]; 
  2. list.map(elem => elem + 1); 
  3. list.map(elem => elem + 2); 

看上去效率有点低,处理函数封装下?
可是map的回调函数只接受当前元素 elem 这一个参数,看上去好像没有办法封装...

你也许会想:如果能拿到一个部分配置好的函数就好了,比如说:

  1. // plus返回部分配置好的函数 
  2. const plus1 = plus(1); 
  3. const plus2 = plus(2); 
  4.  
  5. plus1(5); // => 6 
  6. plus2(7); // => 9 

把这样的函数传进map:

  1. const list = [0, 1, 2, 3]; 
  2. list.map(plus1); // => [1, 2, 3, 4] 
  3. list.map(plus2); // => [2, 3, 4, 5] 

是不是很棒棒?这样一来不管是加多少,只需要list.map(plus(x))就好了,***实现了封装,可读性大大提高! (☆゚∀゚)

不过问题来了:
这样的plus函数要怎么实现呢?

这时候柯里化就能派上用场了:

柯里化函数

  1. // 原始的加法函数 
  2. function origPlus(a, b) { 
  3.   return a + b; 
  4.  
  5. // 柯里化后的plus函数 
  6. function plus(a) { 
  7.   return function(b) { 
  8.     return a + b; 
  9.   } 
  10.  
  11. // ES6写法 
  12. const plus = a => b => a + b; 

可以看到,柯里化的 plus 函数首先接受一个参数 a,然后返回一个接受一个参数 b 的函数,由于闭包的原因,返回的函数可以访问到父函数的参数 a,所以举个例子:const plus2 = plus(2)就可等效视为function plus2(b) { return 2 + b; },这样就实现了部分配置

通俗地讲,柯里化就是一个部分配置多参数函数的过程,每一步都返回一个接受单个参数的部分配置好的函数。一些极端的情况可能需要分很多次来部分配置一个函数,比如说多次相加:

  1. multiPlus(1)(2)(3); // => 6 

这种写法看着很奇怪吧?不过如果入了JS的函数式编程这个大坑的话,这会是常态。(笑)

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

柯里化(Currying)是函数式编程中很重要的一环,很多函数式语言(eg. Haskell)都会默认将函数自动柯里化。然而JS并不会这样,因此我们需要自己来实现自动柯里化的函数。

先上代码:

  1. // ES5 
  2. function curry(fn) { 
  3.   function _c(restNum, argsList) { 
  4.     return restNum === 0 ? 
  5.       fn.apply(null, argsList) : 
  6.       function(x) { 
  7.         return _c(restNum - 1, argsList.concat(x)); 
  8.       }; 
  9.   } 
  10.   return _c(fn.length, []); 
  11.  
  12. // ES6 
  13. const curry = fn => { 
  14.   const _c = (restNum, argsList) => restNum === 0 ? 
  15.     fn(...argsList) : x => _c(restNum - 1, [...argsList, x]); 
  16.  
  17.   return _c(fn.length, []); 
  18.  
  19. /***************** 使用 *********************/ 
  20.  
  21. var plus = curry(function(a, b) { 
  22.   return a + b; 
  23. }); 
  24.  
  25. // ES6 
  26. const plus = curry((a, b) => a + b); 
  27.  
  28. plus(2)(4); // => 6 

这样就实现了自动的柯里化!(╭ ̄3 ̄)╭♡

如果你看得懂发生了什么的话,那么恭喜你!大家口中的大佬就是你!╰(°▽°)╯,快留下赞然后去开始你的函数式生涯吧(滑稽

如果你没看懂发生了什么,别担心,我现在开始帮你理一下思路。

需求分析

我们需要一个 curry 函数,它接受一个待柯里化的函数为参数,返回一个用于接收一个参数的函数,接收到的参数放到一个列表中,当参数数量足够时,执行原函数并返回结果。

实现方式

简单思考可以知道,柯里化部分配置函数的步骤数等于 fn 的参数个数,也就是说有两个参数的 plus 函数需要分两步来部分配置。函数的参数个数可以通过fn.length获取。

总的想法就是每传一次参,就把该参数放入一个参数列表 argsList 中,如果已经没有要传的参数了,那么就调用fn.apply(null, argsList)将原函数执行。要实现这点,我们就需要一个内部的判断函数 _c(restNum, argsList),函数接受两个参数,一个是剩余参数个数 restNum,另一个是已获取的参数的列表 argsList_c 的功能就是判断是否还有未传入的参数,当 restNum 为零时,就是时候通过fn.apply(null, argsList)执行原函数并返回结果了。如果还有参数需要传递的话,也就是说 restNum 不为零时,就需要返回一个单参数函数

  1. function(x) { 
  2.   return _c(restNum - 1, argsList.concat(x)); 

来继续接收参数。这里形成了一个尾递归,函数接受了一个参数后,剩余需要参数数量 restNum 减一,并将新参数 x 加入 argsList 后传入 _c 进行递归调用。结果就是,当参数数量不足时,返回负责接收新参数的单参数函数,当参数够了时,就调用原函数并返回。

现在再来看:

  1. function curry(fn) { 
  2.   function _c(restNum, argsList) { 
  3.     return restNum === 0 ? 
  4.       fn.apply(null, argsList) : 
  5.       function(x) { 
  6.         return _c(restNum - 1, argsList.concat(x)); 
  7.       }; 
  8.   } 
  9.   return _c(fn.length, []); // 递归开始 

是不是开始清晰起来了? (゚▽゚)

ES6写法的由于使用了 数组解构箭头函数 等语法糖,看上去精简很多,不过思想都是一样的啦~

  1. // ES6 
  2. const curry = fn => { 
  3.   const _c = (restNum, argsList) => restNum === 0 ? 
  4.     fn(...argsList) : x => _c(restNum - 1, [...argsList, x]); 
  5.  
  6.   return _c(fn.length, []); 

与其他方法的对比

还有一种大家常用的方法:

  1. function curry(fn) { 
  2.   const len = fn.length; 
  3.   return function judge(...args1) { 
  4.     return args1.length >= len ? 
  5.     fn(...args1): 
  6.     function(...args2) { 
  7.       return judge(...[...args1, ...args2]); 
  8.     } 
  9.   } 
  10.  
  11. // 使用箭头函数 
  12. const curry = fn => { 
  13.   const len = fn.length; 
  14.   const judge = (...args1) => args1.length >= len ? 
  15.     fn(...args1) : (...args2) => judge(...[...args1, ...args2]); 
  16.   return judge; 

与本篇文章先前提到的方法对比的话,发现这种方法有两个问题:

  1. 依赖ES6的解构(函数参数中的 ...args1...args2);

  2. 性能稍差一点。

性能问题

做个测试:

 

  1. console.time("curry");
  2. const plus = curry((a, b, c, d, e) => a + b + c + d + e); 
  3. plus(1)(2)(3)(4)(5); 
  4. console.timeEnd("curry"); 

在我的电脑(Manjaro Linux,Intel Xeon E5 2665,32GB DDR3 四通道1333Mhz,Node.js 9.2.0)上:

  • 本篇提到的方法耗时约 0.325ms

  • 其他方法的耗时约 0.345ms

差的这一点猜测闭包的原因。由于闭包的访问比较耗性能,而这种方式形成了两个闭包fnlen,前面提到的方法只形成了 fn 一个闭包,所以造成了这一微小的差距。

也希望大家能自己测试下并说说自己的看法~ 

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2020-12-03 08:23:23

函数柯里化代码

2023-08-02 08:01:14

柯里化反柯里化

2020-09-23 16:07:52

JavaScript函数柯里化

2021-09-28 07:12:10

avaScriptCurrying柯里化

2024-02-28 16:04:04

深拷贝Python

2024-04-03 15:27:31

Python接口自动化开发

2015-08-19 14:22:01

SQL Server参数

2023-06-28 08:34:02

Bind()函数JavaScript

2010-10-08 10:35:21

2017-11-27 08:54:43

数据信息化智能

2017-11-22 17:41:17

商业智能企业数据

2009-07-02 18:35:56

NAS虚拟化重复删除

2016-09-22 15:50:38

JavascriptRedux源码解析

2017-07-21 09:14:21

2012-11-22 10:19:55

NAS虚拟化

2018-01-12 05:13:35

2016-03-21 10:50:00

宏杉

2013-12-11 10:21:31

2023-10-21 12:52:26

2019-12-23 09:31:11

蓝云
点赞
收藏

51CTO技术栈公众号