函数柯理化的理解与应用

1,777 阅读5分钟

柯理化(currying)

柯理化,也常以为“局部套用”,是把多参数函数转换为一系列单参数函数并进行调用的技术。这项技术以数学家 Haskell Curry 的名字命名。 --- 《JavaScript语言精粹》

  柯理化函数的作用是为每一个逻辑参数返回一个新函数。

初步实现

  先通过一个简单的 add 函数了解一下柯理化。

    // 参数相加
    var add = function(a,b){
        return a+b;
    }
    // 调用
    add(1,2); // => 3

  这是一个加法运算,传入两个值,返回运算结果,如果有这么一个情况,参数a不变,是初始值,仅修改参数b,就会发现有大量的参数重复。

    add(1,2); // => 3
    add(1,3); // => 4
    add(1,4); // => 5

  在这里第一个参数是固定的,如果能使用一种方法讲第一个参数保存下来,或者说记住第一个参数,只需要传入第二个参数,然后返回计算结果。改变这样的现象就需要用到柯理化,先上实例。

    var add = function(a,b){
        var a = a; // 这是一个闭包
        return function(b){
            return a+b;
        }
    }

    var defineAdd = add(1);
    defineAdd(2); // => 3
    defineAdd(3); // => 4
    defineAdd(4); // => 5

实际应用

  这里是一个柯理化实例,利用闭包将第一个参数保存,然后返回新的函数,用于进行加和计算,然后返回最终的结果。可能在实际的项目开发中很少有这样用的。具一个实际项目中能用到的例子,数组元素查询,查询数组中是否包含指定的元素。

    var include = function(val,arr){
        return arr.includes(val);
    }
    // 这里就可以分两种情况
    // 第一种,查找元素在多个数组中的出现
    // 第二种,查找某个数组中的多个元素

    var include1 = function(arr){
        var arr = arr;
        return function(val){
            return arr.includes(val);
        }
    }

    var inArr1 = include1([1,2,3,4]);
    inArr1(1); // => true
    inArr1(2); // => true
    inArr1(3); // => true
    inArr1(4); // => true

    var include2 = function(val){
        var val = val;
        return function(arr){
            return arr.includes(val);
        }
    }

    var inValue = include2(1);
    inValue([1,2,3]); // => true
    inValue([1,3,4]); // => true
    inValue([2,3,4]); // => false

  将一个方法改造之后就可以灵活的应用,避免参数的重复传递,也是充分的发挥了能偷懒则偷懒的原则。上述两个数组元素的查询,也利用到了柯理化当中的向左柯理化和向右柯理化,改变了参数的传递顺序,即可得到不一样的结果。

灵活改变

  柯理化到这里就结束了吗,显然并没有这么简单。在这里虽然利用的柯理化,但是对原函数都进行了重写。那么有没有一种方法可以把原来的函数加工成一个柯理化函数呢。

  在 《JavaScript 设计模式》一书中有这样一个实例,在这里应用一下。

假设我们要编写一个计算每月开销的函数。 在每天结束之前, 我们都要记录今天花掉了多少钱。但我们其实并不太关心每天花掉了多少钱, 而只想知道到月底的时候会花掉多少钱。 也就是说, 实际上只需要在月底计算一次。

    // 我们可以这样去设计一个函数
    // 该函数接收所有的参数,然后进行累加计算
    var cost = function(){
        var money = 0; // 月度花销
        // 将所有的钱数累加
        for(var i = 0, l = arguments.length; i<l;i++){
            money += arguments[i];
        }
        // 返回计算结果
        return money;
    }

    cost(100,200,300,400); // => 1000

  利用上面的方法轻松的完成了每月消费的计算,但是在传入参数的时候就需要先知道本月每一天的花销,再一次性传入函数运算,如何进行修改,我每天都可以传入当天的花销,在月底的时候进行计算。

    var currying = function(fn){
        // 用来保存每次传入的参数数据
        var args = [];
        // 返回一个新的函数,每天都可以传递一个参数
        return function(val){
            // 如果有值,进行保存
            if(arguments.length !== 0){
                args.push(arguments[0]);
            }else{
            // 需要计算,调用函数,进行计算,返回结果
               return fn.apply(null,args);
            }
        }
    }

    // 原计算函数
    var cost = function(){
        var money = 0;
        for(var i = 0, l = arguments.length; i<l;i++){
            money += arguments[i];
        }
        return money;
    }

    // 函数柯理化处理
    cost = currying(cost);
    cost(100); // 无计算
    cost(200); // 无计算
    cost(300); // 无计算
    cost(400); // 无计算
    cost(); // => 1000

  在不改变原函数的情况下进行该写,同样也实现了对每一个参数的分别处理。

通用方案

  到这里就不得不说一个经典的面试题了 将add(1,2,3)改写成add(1)(2)(3)。再之前看到这个题目是一脸懵逼的,我都不知道还有这样的操作,在学习完柯理化之后然我们来秀一把。

    var add = function (a, b, c) {
        return a + b + c;
    }

    var currying = function (fn) {
        // 在这里可以先验证一下,如果传入的参数不是函数,就直接返回,无需处理
        if (typeof fn !== 'function') return fn;

        return function func(...args) {
            // 如果接收的参数个数不满足原函数参数个数的要求,继续返回函数用于接收参数
            if (args.length < fn.length) {
                return function (...argss) {
                    // 递归调用,重复判断
                    return func.call(null, ...args, ...argss);
                }
            } else {
                // 满足条件则计算
                return fn.apply(null, args)
            }
        }
    }

    add = currying(add);
    add(1, 2, 3); // => 6
    add(1, 2)(3); // => 6
    add(1)(2, 3); // => 6
    add(1)(2)(3); // => 6

大佬代码参考

  其实上述案例中的currying已经能够满足多数函数柯理化场景。最后附上简化整理版本,这个我就写不出来了。

参考 编写一个通用的柯里化函数currying

    const currying = (fn, ...args) =>
    args.length < fn.length
        ? (...arguments) => currying(fn, ...args, ...arguments)
        : fn(...args);

参考: 《JavaScript 设计模式》 3.2.4 高阶函数的其他应用
《JavaScript 函数式编程》 5.2 柯理化
《JavaScript 语言精粹》 第四章 柯理化