美文网首页
JavaScript 函数柯里化和偏函数

JavaScript 函数柯里化和偏函数

作者: 卓三阳 | 来源:发表于2018-12-04 18:17 被阅读18次
    1.柯里化

    柯里化(英语:Currying),又译为卡瑞化加里化。在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术

    如果我们需要实现一个求三个数之和的函数:

    function add(a, b, c) {
      return a + b + c;
    }
    console.log(add(2, 3, 5)); // 10
    

    如果按照柯里化思想,我们最先想到可能是add函数执行后返回一个函数对象,这个函数对象执行后再返回一个新函数,这样一直执行下去

    function add(a) {  
       return function(b) {
         return function(c) {
           return a+b+c;
         }
       }
    }
    console.log(add(2)(3)(5))  //10
    

    但是,这种方法麻烦缺乏拓展性,我们必须知道调用次数。如果调用次数不确定或者很多是行不通的。另外,如果我们我们要实现add(2,3,5)和add(2,3)(5)也是输出10就更加不行


    1.1实现对有特定参数个数的函数柯里化

    addCurry(2)(3)(5),addCurry(2,3)(5)和addCurry(2,3,5)输出10

    var curry = function(func){
    
        var length = func.length,
        args = Array.prototype.slice.call(arguments, 1)||[];
        return function(){
            var newArgs = args.concat([].slice.call(arguments));
            if(newArgs.length < length){
                return curry.call(this,func,...newArgs);
            }else{
                return func.apply(this,newArgs);
            }
        }
    
    }
    var addCurry=curry(function (a,b,c){
        return a+b+c;
    })
    console.log(addCurry(2)(3)(5))  //10
    console.log(addCurry(2,3)(5))   //10
    console.log(addCurry(2,3,5))    //10
    
    

    这里代码逻辑其实很简单,就是判断参数是否已经达到预期的值(函数柯里化之前的参数个数),如果没有继续返回函数,达到了就执行函数然后返回值。但这一版柯里化函数仍然不能完全满足要求,因为它只针对有特定参数个数的函数适用

    我们来看一个常见的例子:比如开发者编写代码的时候会在应用的不同阶段编写很多日志。我们可以编写一个如下的日志函数:

    const loggerHelper = (mode,initialMessage,errorMessage,lineNo) => {
        if(mode === "DEBUG"){
            console.debug(initialMessage,errorMessage + "at line:" + lineNo)
        }else if(mode === "ERROR"){
            console.error(initialMessage,errorMessage + "at line:" + lineNo)
        }else if(mode === "WARN"){
            console.warn(initialMessage,errorMessage + "at line:" + lineNo)
        }else{
            throw "Wrong mode"
        }
    }
    

    当团队中的任何开发者需要向控制台打印 Stats.js 文件中的错误时,可以用如下方式使用函数

    loggerHelper("ERROR","Error At Stats.js","Invalid argument passed",23)
    loggerHelper("ERROR","Error At Stats.js","undefined argument",223)
    loggerHelper("ERROR","Error At Stats.js","curry function is not defined",3)
    loggerHelper("ERROR","Error At Stats.js","slice is not defined",31)
    

    现在我们可以用 curry 函数重写这个函数了,下面通过 curry 解决重复使用前两个参数的问题

    let errorLogger = curry(loggerHelper)("ERROR")("Error At Stats.js")
    let debugLogger = curry(loggerHelper)("DEBUG")("Debug At Stats.js")
    let warnLogger = curry(loggerHelper)("WARN")("Warn At Stats.js")
    

    现在我们能够轻松使用上面的柯里化函数并在各自的上下文中使用它们了

    // 用于错误
    errorLogger("Error message",21)
    // Error At Stats.js Error message at line:21
    
    // 用于调试
    debugLogger("Debug message",223)
    // Debug At Stats.js Debug message at line:223
    
    // 用于警告
    warnLogger("Warn message",34)
    // Warn At Stats.js Warn message at line:223
    

    经过这个过程我们发现,柯里化能够应对更加复杂的逻辑封装。当情况变得多变,柯里化依然能够应付自如。


    1.2 Currying(柯里化) variadic(可变参数) 函数

    在我看来,这不是重点,没有什么通用性,通常用于封装特定的函数,可跳过

    1.2.1 将传入参数为空设置作为函数执行返回值的条件
    var curry=function(func){
    
        var length=func.length,
            args=Array.prototype.slice.call(arguments,1);
            return function(){
                 var newArgs=args.concat([].slice.call(arguments));
                 if(arguments.length!==0){
                     return curry.call(this,func,...newArgs)
                 }else{
                     return  func.apply(this,newArgs) 
                 }
            }
    
    
    }
    var addCurry=curry(function(){
         var args=[].slice.call(arguments),
             sum=0;
         args.map(val=>sum+=val)
         return sum;
    })
    console.log(addCurry(1,2,3)()) //6
    console.log(addCurry(1)(2)(3)(4)())  //10
    console.log(addCurry(1)(2)(2,3)(4)())  //12
    

    但这一版柯里化函数仍然不能完全满足要求,因为只有设置的传入参数为空时候才能触发函数执行返回值

    1.2.2 利用函数的函数的隐式转换

    addCurry(1,2,3) //6
    addCurry(1)(2)(3)(4) //10
    addCurry(1)(2)(3)(4) //12

    其实这里的需求是我们在柯里化的过程中既能返回一个函数继续接受剩下的参数,又能就此输出当前的一个结果。

    补充一个知识点是函数的隐式转换。当我们直接将函数参与其他的计算时,函数会默认调用toString方法,直接将函数体转换为字符串参与计算

    function fn() { return 20 }
    console.log(fn + 10);     // 输出结果 function fn() { return 20 }10
    

    但是我们可以重写函数的toString方法,让函数参与计算时,输出我们想要的结果

    function fn() { return 20; }
    fn.toString = function() { return 20 }
    console.log(fn + 10); // 30
    

    除此之外,当我们重写函数的valueOf方法也能够改变函数的隐式转换结果。当我们同时重写函数的toString方法与valueOf方法时,最终的结果会取valueOf方法的返回结果。

    add方法的实现仍然会是一个参数的收集过程。当add函数执行到最后时,仍然返回的是一个函数,但是我们可以通过定义toString/valueOf的方式,让这个函数可以直接参与计算,并且转换的结果是我们想要的。而且它本身也仍然可以继续执行接收新的参数。实现方式如下:

    
    function add(){
        var args = [].slice.call(arguments);
        var fn = function(){
            var newArgs = args.concat([].slice.call(arguments));
            return add.apply(this,newArgs);
        } 
        fn.toString = function(){
            return args.reduce(function(a, b) {
                return a + b;
            })
        }
        return fn ;
    }
    
    console.log(add(1)(2)(3)+0) //6
    console.log(add(1,2,3)+0)   //6
    console.log(add(1)(2,3)(3)+0) //9
    
    

    2.Partial Application(偏函数)

    在计算机科学中,局部应用是指固定一个函数的一些参数,然后产生另一个更小元的函数

    偏函数与柯里化区别:
      (1)柯里化是将一个多参数函数转换成多个单参数函数,也就是将一个 n 元函数转换成 n 个一元函数
      (2)局部应用则是固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数

    柯里化可以看做偏函数的一种特殊的应用

    有这样的一个场景:我们需要对多个不同的接口发起HTTP请求

    function ajax(url, data, callback) {
      // ..
    }
    

    如果直接调用该函数,每一次调用都很麻烦。我们可能产生如下调用方式:

    function ajaxTest1(data, callback) {
      ajax('http://www.test.com/test1', data, callback);
    }
    
    function ajaxTest2(data, callback) {
      ajax('http://www.test.com/test2', data, callback);
    }
    

    我们通过ajaxTest1()把原函数ajax()的参数个数从3个减少到了2个

    利用偏函数,我们可以这样做,我们这样定义一个partial()函数:

    function partial(fn, ...presetArgs) {
      return function partiallyApplied(...laterArgs) {
            let allArgs =presetArgs.concat(laterArgs)
            return fn.apply(this, allArgs)
      }
    }
    

    partial()函数接收fn参数,来表示被我们偏应用实参(partially apply)的函数。接着,fn形参之后,presetArgs数组收集了后面传入的实参,保存起来稍后使用。

    我们创建并return了一个新的内部函数,该函数中,laterArgs数组收集了全部实参。
    使用偏函数的这种模式,我们重构之前的代码:

    function ajax(url, data, callback) {
      // ..
    }
    
    var ajaxTest1 = partial(ajax, 'http://www.test.com/test1');
    var ajaxTest2 = partial(ajax, 'http://www.test.com/test2');
    
    

    3.柯里化或偏函数有什么用?

    柯里化或偏函数主要是对于参数进行一些操作,将多个参数转换为单一参数或者减少参数个数的过程。如果参数不足的话它们就会处在一种中间状态,我们可以利用这种中间状态做任何事!!!而传统函数调用则需要预先确定所有实参。如果你在代码某一处只获取了部分实参,然后在另一处确定另一部分实参,这个时候柯里化和偏应用就能派上用场。

    归纳下来,主要为以下常见的三个用途:

    (1)动态生成函数
    (2)减少参数
    (3)延迟计算


    参考

    JavaScript函数柯里化

    相关文章

      网友评论

          本文标题:JavaScript 函数柯里化和偏函数

          本文链接:https://www.haomeiwen.com/subject/eyljcqtx.html