朋友,柯里化(Currying)了解一哈

该文章是直接翻译国外一篇文章,关于柯里化(Currying)。
都是基于原文处理的,其余的都是直接进行翻译可能有些生硬,因此为了行文方便,就作了一些简单的本地化处理。
若是想直接根据原文学习,能够忽略此文。javascript

若是你以为能够,请多点赞,鼓励我写出更精彩的文章🙏。java

案例引导:

如今有以下需求,将多参函数转换为n个单参函数的组合。若是没有想到合适的方式来实现,巧了不是嘛,这不是巧了嘛。这篇文章就是为了说明白这个问题的。spring

Talk is cheap,show you the code

//原函数
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:

  1. 你能够选择一次传入多个参数。这中处理方式在开发应用中颇有用。
  2. 正如上面声明,你能够在调用函数过程当中,传入空参,它也会返回一个等待剩余参数的函数

手动实现一个柯里化函数

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]);
  };
};
复制代码

打开控制台,咱们来一块儿欣赏一下这段奇妙的代码。

准备工做

greetcurry复制到控制台。而后输入curriedGreet = curry(greet),而后开启这段奇妙之旅吧。

第一次停顿(代码第二行)

经过监听函数的两个参数,咱们能够看到 originalFunction就是 greet而且因为咱们没有提供第二个参数,因此 initialParams的值仍是在定义函数时候的默认值。移动到下一个断点处。

猛然发现断点直接跳出函数做用域,也就是curry(greet)返回了一个等待(N>3)的函数。在控制台判断返回值的类型,也和咱们分析的同样。

而且咱们继续调用返回的函数,并存于sayHello变量中。

sayHello = curriedGreet('Hello')
复制代码

而且在控制台执行它。

第二次停顿(代码第四行)

在进行第二次停顿以前,咱们查看了监听的 originalFunctioninitialParams。可是在第一次断点以后,就返回了一个 函数,为何在新函数的做用域中,也能够访问到这些变量呢?这是由于该新函数是从父级函数中返回的,可以访问父级函数中定义变量。(或者用更加通俗的话来说,这是 闭包,关于闭包,会专门有一篇文章,进行讲解,如今在筹备过程当中。敬请期待)

父子函数之间的参数继承关系

当一个父级函数调用以后,他们会将本身参数留给子孙级函数所使用。这种继承方式和现实方式中继承是同样的。

curry在定义/初始化的时候,就将originalFunctioninitialParams做为初始参数,随后返回了一个子函数(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']。因而可知,initialParamsnextParams都为数组,因此,能够经过spread operator将两个数组进行合并处理。

关键点,就在这里。

若是 params and originalFunction具备相同长度,将会直接调用 greet,也就意味着柯里化过程完成了。

JS函数也具备length属性

这也是可以完成柯里化的关键步骤。此处就判断返回的函数是否继续等待剩余参数。(提早透露下,这里是结束递归的判断,若是没有这个,将直接致使死循环)

在JS中,一个函数的.length属性用于标识在函数定义的时候,有几个参数。或者说,函数期待几个参数参与函数运行。

greet.length; // 3

iTakeOneParam = (a) => {};
iTakeTwoParams = (a, b) => {};

iTakeOneParam.length; // 1
iTakeTwoParams.length; // 2

复制代码

若是你提供了函数指望的参数个数,那柯里化工做直接返回原始函数而且不在进行其他操做。

可是,咱们提供的示例中,parameters的长度是和函数长度不同的。咱们仅仅提供了Hello,因此parameters长度为1,可是originalFunction.length3。因此此处的if()判断是false。咱们将走的是另一个分支。从新调用主函数curry(也就是进入了递归处理了),而此时curry()接收greetHello为参数,从新走上面的流程。

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.length2仍是没有达到预期的数值,继续递归处理。

||
                                        ||
                                        ||
                                        \/
复制代码

因此咱们继续基于sayHelloToFan进行处理。sayHelloToFanBeichen = sayHelloToFan('北宸')

继续开始了参数处理和参数判断之旅。可是此时有一点不一样了。就是在判断参数数组长度和originalFunction.length时候。

||
                                    ||
                                    ||
                                    \/
复制代码

此时的话,就和调用greet('Hello','范','北宸')的效果和结果是同样的。

大结局

greet获取了它应获取的参数,curry也中止了递归处理。而且,咱们也得到了想要的结果Hello,范北宸

其实利用currygreet通过如上处理以后,如今处理以后的函数可以同时接收任意(n≥0)的参数。

curriedGreet('Hello', '范', '北宸');
curriedGreet('Hello', '范')('北宸');
curriedGreet()()('Hello')()('范')()()()()('北宸');

复制代码

补丁

因为在这篇文章发文以后,有一些小伙伴问,curry的好处也好啊,仍是如何应用到实际工做中啊。其实这篇文章只是单纯的介绍如何用JS实现curry

而有一点须要你们明确,curry函数式编程中的一个重要概念。若是说实际中用到这个编程方式了吗,说实话,我没有。可是通过翻阅一些资料,打算之后项目中会尝试使用。

因此,给你们列举一下我查找的相关资料(其实就是函数式编程的官网的一些介绍文章)

  1. 爱上柯里化
  2. 为何柯里化有帮助
  3. 手动实现一个Redux Redux是React项目开发中比较经常使用的一个库。
相关文章
相关标签/搜索