该文章是直接翻译国外一篇文章,关于柯里化(Currying)。
都是基于原文处理的,其余的都是直接进行翻译可能有些生硬,因此为了行文方便,就作了一些简单的本地化处理。
若是想直接根据原文学习,能够忽略此文。javascript
若是你以为能够,请多点赞,鼓励我写出更精彩的文章🙏。java
如今有以下需求,将多参函数转换为n个单参函数的组合。若是没有想到合适的方式来实现,巧了不是嘛,这不是巧了嘛。这篇文章就是为了说明白这个问题的。spring
//原函数
add=(first,second,third)=>first+second+third;
//函数改造
add(1)(2)(3) //6
add(1,2)(3) //6
add(1)()()(2,3) //6
复制代码
咱们先来看一下关于Currying的定义(该定义被计算机科学和数学都承认)编程
(Currying将多参函数转换为单参函数)
Currying turns multi-argument functions into unary (single argument) functions.redux
被柯里化的原函数,一次能够接收多个参数。就像下面同样:数组
greet = (greeting, first, last) => `${greeting}, ${first} ${last}`;
greet('Hello', '范', '北宸'); // Hello, 范北宸
复制代码
对原函数(greet
)进行适当的柯里化处理以后闭包
curriedGreet = curry(greet);
curriedGreet('Hello')('范')('北宸'); // Hello, 范北宸
复制代码
这个三元函数已经被改造为三个一元函数。当你提供了一个参数,一个期待下一个参数的新的函数被返回。函数式编程
上面之因此说适当的柯里化是由于一些柯里化函数在使用的时候是很是灵活的。柯里化伟大之处在于理论思惟,可是在JS中为构建/调用一个函数为了处理每一个参数将变的很棘手。函数
Ramda’s 柯里化函数可让你经过下面的方式来调用curriedGreet
:post
// greet 须要三个参数: (greeting, first, last)
// 这些都将返回一个函数,等待剩余参数(first, last)
curriedGreet('Hello');
curriedGreet('Hello')();
curriedGreet()('Hello')()();
// 这些都将返回一个函数,等待剩余参数(last)
curriedGreet('Hello')('范');
curriedGreet('Hello', '范');
curriedGreet('Hello')()('范')();
// 当参数个数符合最初定义的时候,将会返回最后结果,这些将返回一个字符串
curriedGreet('Hello')('范')('北宸');
curriedGreet('Hello', '范', '北宸');
curriedGreet('Hello', '范')()()('北宸');
复制代码
Notice:
Mr. Elliot 分享了一个和Ramda
相似的curry
实现。
const curry = (f, arr = []) => (...args) =>
((a) => (a.length === f.length ? f(...a) : curry(f, a)))([...arr, ...args]);
复制代码
是否是很惊奇。心中是否万马奔腾。这玩意就能实现curry
。
将ES6的箭头函数替换为更加看懂的方式,同时新增了debugger
,便于在分析调用过程。
curry = (originalFunction, initialParams = []) => {
debugger;
return (...nextParams) => {
debugger;
const curriedFunction = (params) => {
debugger;
if (params.length === originalFunction.length) {
return originalFunction(...params);
}
return curry(originalFunction, params);
};
return curriedFunction([...initialParams, ...nextParams]);
};
};
复制代码
打开控制台,咱们来一块儿欣赏一下这段奇妙的代码。
将greet
和curry
复制到控制台。而后输入curriedGreet = curry(greet)
,而后开启这段奇妙之旅吧。
originalFunction
就是
greet
而且因为咱们没有提供第二个参数,因此
initialParams
的值仍是在定义函数时候的默认值。移动到下一个断点处。
猛然发现断点直接跳出函数做用域,也就是curry(greet)
返回了一个等待(N>3)的函数。在控制台判断返回值的类型,也和咱们分析的同样。
而且咱们继续调用返回的函数,并存于sayHello
变量中。
sayHello = curriedGreet('Hello')
复制代码
而且在控制台执行它。
originalFunction
和
initialParams
。可是在第一次断点以后,就返回了一个
新函数,为何在新函数的做用域中,也能够访问到这些变量呢?这是由于该新函数是从父级函数中返回的,可以访问父级函数中定义变量。(或者用更加通俗的话来说,这是
闭包,关于闭包,会专门有一篇文章,进行讲解,如今在筹备过程当中。敬请期待)
当一个父级函数调用以后,他们会将本身参数留给子孙级函数所使用。这种继承方式和现实方式中继承是同样的。
curry
在定义/初始化的时候,就将originalFunction
和initialParams
做为初始参数,随后返回了一个子函数(child)。所以这两个变量没有被销毁,由于子函数也对其有访问权限。
经过监听nextParams
咱们突然发现,该值为['Hello']
。可是咱们在调用curriedGreet()
的时候,是传入的'Hello'
而不是['Hello']
。
谜底:咱们在调用curriedGreet
的时候,传入的是'Hello'
,可是经过rest syntax,咱们将'Hello'
转换为['Hello']
。
curry 是一个能够接收N(N>1,10,100)个参数的函数,因此经过rest syntax
处理以后,函数可以轻松的访问这些参数。既然每次都是传入一个参数,经过rest syntax
每次都将参数捕获到数组中。
继续移动断点。
在运行第六行的debugger
以前,是先调用12行的。咱们在第五行定义了一个名为curriedFunction
的函数,在12行处调用他。因此咱们将断点放置在了方法体内。那调用curriedFunction
的时候,传入的数据是啥呢?
[...initialParams, ...nextParams];
复制代码
在第五行咱们查看了参数...nextParams
为['Hello']
。因而可知,initialParams
和nextParams
都为数组,因此,能够经过spread operator
将两个数组进行合并处理。
关键点,就在这里。
params
and
originalFunction
具备相同长度,将会直接调用
greet
,也就意味着柯里化过程完成了。
这也是可以完成柯里化的关键步骤。此处就判断返回的函数是否继续等待剩余参数。(提早透露下,这里是结束递归的判断,若是没有这个,将直接致使死循环)
在JS中,一个函数的.length
属性用于标识在函数定义的时候,有几个参数。或者说,函数期待几个参数参与函数运行。
greet.length; // 3
iTakeOneParam = (a) => {};
iTakeTwoParams = (a, b) => {};
iTakeOneParam.length; // 1
iTakeTwoParams.length; // 2
复制代码
若是你提供了函数指望的参数个数,那柯里化工做直接返回原始函数而且不在进行其他操做。
可是,咱们提供的示例中,parameters
的长度是和函数长度不同的。咱们仅仅提供了Hello
,因此parameters
长度为1,可是originalFunction.length
为3。因此此处的if()
判断是false
。咱们将走的是另一个分支。从新调用主函数curry
(也就是进入了递归处理了),而此时curry()
接收greet
和Hello
为参数,从新走上面的流程。
curry
本质上是一个无限循环的自我调用而且对参数贪婪的函数,直到函数个数===originalFunction.length
才会中止。
这是在对greet
进行柯里化处理时的参数快照curriedGreet = curry(greet)
这是在greet
柯里化以后而且接受一个参数以后的参数快照sayHello = curriedGreet('Hello')
很显然,第二次运行到第二行的时候,参数变化了,也就是originalFunction
仍是greet
,可是如今initialParams
变成了['Hello']
了,而不是空数组了。
而后继续跳过断点,又双叒叕返回了一个全新的函数(sayHello
),而这个函数也期待这剩余函数的传入。 sayHelloToFan = sayHello('范').
继续跟踪断点,又跳到第四行,此时nextParams
为['范']
。
curriedFunction
的参数为
['Hello', '范']
在12行有进行数组合并的处理[...initialParams, ...nextParams]
,而initialParams
为[Hello]
,nextParams
是经过...nextParams
操做以后,将范
转换为['范']
。因此,在12行的时候,就是针对两个数组进行合并处理[...['Hello'],...['范']
。
如今又到了curriedFunction
抉择的时候了,此时params.length
为2仍是没有达到预期的数值,继续递归处理。
||
||
||
\/
复制代码
因此咱们继续基于sayHelloToFan
进行处理。sayHelloToFanBeichen = sayHelloToFan('北宸')
继续开始了参数处理和参数判断之旅。可是此时有一点不一样了。就是在判断参数数组长度和originalFunction.length
时候。
||
||
||
\/
复制代码
此时的话,就和调用greet('Hello','范','北宸')
的效果和结果是同样的。
greet
获取了它应获取的参数,curry
也中止了递归处理。而且,咱们也得到了想要的结果Hello,范北宸
。
其实利用curry
对greet
通过如上处理以后,如今处理以后的函数可以同时接收任意(n≥0)的参数。
curriedGreet('Hello', '范', '北宸');
curriedGreet('Hello', '范')('北宸');
curriedGreet()()('Hello')()('范')()()()()('北宸');
复制代码
因为在这篇文章发文以后,有一些小伙伴问,curry
的好处也好啊,仍是如何应用到实际工做中啊。其实这篇文章只是单纯的介绍如何用JS实现curry
。
而有一点须要你们明确,curry
是函数式编程中的一个重要概念。若是说实际中用到这个编程方式了吗,说实话,我没有。可是通过翻阅一些资料,打算之后项目中会尝试使用。
因此,给你们列举一下我查找的相关资料(其实就是函数式编程的官网的一些介绍文章)