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


    一、什么是柯里化?

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

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

    二、柯里化应用

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

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

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

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

      看上去效率有点低,处理函数封装下?

      可是map的回调函数只接受当前元素 elem 这一个参数,看上去好像没有办法封装。你也许会想:如果能拿到一个部分配置好的函数就好了,比如说:

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

      把这样的函数传进map:

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

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

      不过问题来了:这样的plus函数要怎么实现呢?这时候柯里化就能派上用场了:

    三、柯里化函数

    // 原始的加法函数
    function origPlus(a, b) {
      return a + b;
    }
    
    // 柯里化后的plus函数
    function plus(a) {
      return function(b) {
        return a + b;
      }
    }
    
    // ES6写法
    const plus = a => b => a + b;

      可以看到,柯里化的 plus 函数首先接受一个参数 a,然后返回一个接受一个参数 b 的函数,由于闭包的原因,返回的函数可以访问到父函数的参数 a。

      所以举个例子:const plus2 = plus(2),就可等效视为function plus2(b) { return 2 + b; },这样就实现了部分配置。

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

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

      这种写法看着很奇怪吧?不过如果熟悉JS的函数式编程的话,这会是常态。

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

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

    // ES5
    function curry(fn) {
      function _c(restNum, argsList) {
        return restNum === 0 ?
          fn.apply(null, argsList) :
          function(x) {
            return _c(restNum - 1, argsList.concat(x));
          };
      }
      return _c(fn.length, []);
    }
    
    // ES6
    const curry = fn => {
      const _c = (restNum, argsList) => restNum === 0 ?
        fn(...argsList) : x => _c(restNum - 1, [...argsList, x]);
    
      return _c(fn.length, []);
    }
    
    /***************** 使用 *********************/
    
    var plus = curry(function(a, b) {
      return a + b;
    });
    
    // ES6
    const plus = curry((a, b) => a + b);
    
    plus(2)(4); // => 6

      这样就实现了自动的柯里化!我们现在开始理一下思路:

    1、需求分析

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

    2、实现方式

      简单思考可以知道,柯里化部分配置函数的步骤数等于 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 进行递归调用。结果就是,当参数数量不足时,返回负责接收新参数的单参数函数,当参数够了时,就调用原函数并返回。

      现在再来看

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

      是不是开始清晰起来了

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

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

    3、与其他方法的对比

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

    function curry(fn) {
      const len = fn.length;
      return function judge(...args1) {
        return args1.length >= len ?
        fn(...args1):
        function(...args2) {
          return judge(...[...args1, ...args2]);
        }
      }
    }
    
    // 使用箭头函数
    const curry = fn => {
      const len = fn.length;
      const judge = (...args1) => args1.length >= len ?
        fn(...args1) : (...args2) => judge(...[...args1, ...args2]);
      return judge;
    }

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

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

      (2)性能稍差一点。

    4、性能问题

      做个测试:

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

      本篇提到的方法耗时约 0.550ms,其他方法的耗时约 2.309ms

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

  • 相关阅读:
    2018-2019-2 20175120 实验四《Android程序设计》实验报告
    mini dc(选做)
    20175120彭宇辰 《Java程序设计》第十一周学习总结
    20175120彭宇辰 《Java程序设计》第十周学习总结
    实验三 《敏捷开发与XP实践》实验报告
    20175120彭宇辰 《Java程序设计》第九周学习总结
    20175120彭宇辰 《Java程序设计》第八周学习总结
    20175120彭宇辰-结对编程-四则运算(二)
    实验二《面向对象程序设计》实验报告
    20175120彭宇辰 《Java程序设计》第七周学习总结
  • 原文地址:https://www.cnblogs.com/goloving/p/8035450.html
Copyright © 2020-2023  润新知