JS函数柯里化(curry)和函数合成(compose)

JavaScript 函数式编程有两种最基本的运算:compose(函数合成)和 curry(柯里化)。

函数合成

【问题提出】

在 JS 函数式编程中,经常见到如下表达式运算。

a(b(c(x)));

这是“包菜式”多层函数调用,但不是很优雅。为了解决函数多层调用的嵌套问题,我们需要用到函数合成。其语法格式如下:

var f = compose(a, b, c);  //合成函数
f(x);

例如:

var compose = function (f, g) {
    return function (x) {
        return f(g(x));
    };
};
var add = function (x) { return x + 1; }  //加法运算
var mul = function (x) { return x * 5; }  //乘法运算
compose(mul, add) (2);  //合并加法运算和乘法运算,返回15

在上面代码中,compose 函数的作用就是组合函数,将函数串联起来执行,一个函数的输出结果是另一个函数的输入参数,一旦第 1 个函数开始执行,就会像多米诺骨牌一样推导执行了。

使用 compose 要注意以下 3 点:

  • compose 的参数是函数,返回的也是一个函数。
  • 除了初始函数(最右侧的一个)外,其他函数的接收参数都是一个函数的返回值,所以初始函数的参数可以是多元的,而其他函数的接收值是一元的。
  • compose 函数可以接收任意的参数,所有的参数都是函数,且执行方向为自右向左。初始函数一定要放到参数的最右侧。

下面来完善 compose 实现,实现无限函数合成。

既然函数以多米诺骨牌式执行,那么可以使用递归或迭代,在函数体内不断地执行 arguments 中的函数,将上一个函数的执行结果作为下一个执行函数的输入参数。

//函数合成,从右到左合成函数
var compose = function () {
    var _arguments = arguments;  //缓存外层函数
    var length = _arguments.length;  //缓存长度
    var index = length;  //定义游标变量
    //检测函数,如果存在非函数参数,则抛出异常
    while (index --) {
        if (typeof _arguments[index] !== 'function') {
            throw new TypeError('参数必须为函数!');
        }
    }
    return function () {
        var index = length - 1;  //定位到最后一个参数下标
        //如果存在两个及以上参数,则调用最后一个参数函数,并传入内层函数;否则直接返回第 1 个参数函数。
        var result = length ? _arguments[index].apply(this, arguments) : arguments[0];
        //迭代参数函数
        while (index -- ) {
            //把右侧函数的执行结果作为参数传给左侧参数函数,并调用。
            result = _arguments[index].call(this, result);
        }
        return result;  //返回最左侧参数函数的执行结果
    }
}
//反向函数合成,即从左到右合成函数
var composeLeft = function () {
    return compose.apply(null, [].reverse.call(arguments));
}

在上面实现代码中,compose 实现是从右到左进行合成,也提供了从左到右的合成,即 composeLeft,同时在 compose 体内添加了一层函数的校验,允许传递一个或多个参数。

var add = function (x) { return x + 5; }  //加法允许
var mul= function (x) { return x * 5; }  //乘法运算
var sub= function (x) { return x - 5; }  //减法运算
var div = function (x) { return x / 5; }  //除法运算
var fn = compose(add, mul, sub, div);
console.log(fn(50));  //返回30
var fn = compose(add, compose(mul, sub, div));
console.log(fn(50));  //返回30
var fn = compose(compose(add, mul), sub, div);
console.log(fn(50));  //返回30

上面几种组合方式都可以,最后都返回 30。注意,排列顺序要保持一致。

函数柯里化

【问题提出】

JS 函数合成是把多个单一参数函数合成一个多参数函数的运算。例如,a(x) 和 b(x) 组合为 a(b(x)),则合称为 f(a,b,x)。注意,这里的 a(x) 和 b(x) 都只能接收一个参数。如果接收多个参数,如 a(x,y) 和 b(a,b,c),那么函数合成就比较麻烦。

这时就要用到函数柯里化。所谓柯里化,就是把一个多参数的函数转化为单一参数函数。有了柯里化运算之后,我们就能做到所有函数只接收一个参数。

【设计思路】

先用传递给函数的一部分参数来调用它,让它返回一个函数,然后再去处理剩下的参数。也就是说,把多参数的函数分解为多步操作的函数,以实现每次调用函数时,仅需要更少或单个参数。例如,下面是一个简单的求和函数 add()。

var add = function (x,y) {
    return x + y;
}

每次调动 add(),需要同时传入两个参数。如果希望每次仅传入一个参数,可以这样进行柯里化。

var add = function (x) {  //柯里化
    return function (y) {
        return x + y;
    }
}
console.log(add(2) (6));  //8,连续调用
var add1 = add(200);
console.log(add1(2));  //202,分步调用

函数 add 接收一个参数,并返回一个函数,这个返回的函数可以再接收一个参数,并返回两个参数之和。从某种意义上讲,这是一种对参数的“缓存”,是一种非常高效的函数式运算方法。柯里化在 DOM 的回调中非常有用。

【实现代码】

设想 curry 可以接收一个函数,即原始函数,返回的也是一个函数,即柯里化函数。返回这个柯里化函数在执行过程中会不断地返回一个存储了传入参数的函数,直到触发了原始函数执行的条件。例如,设计一个 add() 函数,计算两个参数之和。

var add = function (x,y) {
    return x + y;
}

柯里化函数:

var curryAdd = curry(add);

这个 add 需要两个参数,但是执行 curryAdd 时,可以传入更少的参数。当传入的参数少于 add 需要的参数时,add 函数并不会执行,curryAdd 就会将这个参数记录下来,并且返回另外一个函数,这个函数可以继续执行传入参数。如果传入参数的总数等于 add 所需参数的总数,则执行原始参数,返回想要的结果。如果没有参数限制,最后根据空的小括号作为执行原始参数的条件,返回运算结果。

curry 实现的封装代码如下:

//柯里化函数
function curry (fn) {
    var _argLen = fn.length;  //记录原始函数的形参个数
    var _args = [].slice.call(arguments, 1);  //把传入的第2个及以后参数转换为数组
    function wrap () {  //curry函数
        //把当前参数转换为数组,与前面参数进行合并
        _args = _args.concat([].slice.call(arguments));
        function act () {  //参数处理函数
            //把当前参数转换为数组,与前面参数进行合并
            _args = _args.concat([].slice.call(arguments));
            //如果传入参数总和大于等于原始参数的个数,触发执行条件
            if ((_argLen == 0 && arguments.length == 0) ||
                (_argLen > 0 && _args.length >= _argLen)) {
                //执行原始函数,并把每次传入参数传入进去,返回执行结果,停止curry
                return fn.apply(null, _args);
            }
            return arguments.callee;
        }
        //如果传入参数大于等于原始函数的参数个数,即触发了执行条件
        if ((_argLen == 0 && arguments.length == 0) ||
            (_argLen > 0 && _args.length >= _argLen)) {
            //执行原始函数,并把每次传入参数传入进去,返回执行结果,停止curry
            return fn.apply(null, _args);
        }
        act.toString = function () {  //定义处理函数的字符串表示为原始函数的字符串表示
            return fn.toString();
        }
        return act;  //返回处理函数
    }
    return wrap;  //返回curry函数
}

【应用代码】

1) 应用函数无形参限制
设计求和函数,没有形参限制柯里化函数将根据小括号作为最后调用原始函数的条件。

//求和函数,参数不限
var add = function () {
    //迭代所有参数值,返回最后汇总的值
    return [].slice.call(arguments).reduce(function (a,b) {
        //如果元素的值为数值,则参与求和运算,否则设置为0,跳过非数字的值
        return (typeof a == "number" ? a : 0) + (typeof b =="number" ? b : 0);
    })
}
//柯里化函数
var curried = curry(add);
console.log(curried(1) (2) (3) ());  //6
var curried = curry(add);
console.log(curried(1,2,3) (4) ());  //10
var curried = curry(add, 1);
console.log(curried(1,2) (3) (3) ());  //10
var curried = curry(add, 1, 5);
console.log(curried(1,2,3,4) (5) ());  //21

2) 应用函数有形参限制
设计求和函数,返回 3 个参数之和。

var add = function (a,b,c) {  //求和函数,3个参数之和
    return a + b + c;
}
//柯里化函数
var curried = curry(add, 2);
console.log(curried(1) (2));  //5
var curried = curry(add, 2, 1);
console.log(curried(2));  //5
var curried = curry(add);
console.log(curried (1) (2) (6));  //9
var curried = curry(add);
console.log(curried(1,2,6));  //9

curry 函数的设计不是固定的,可以根据具体应用场景灵活定制。curry 主要有 3 个作用:缓存函数、暂缓函数执行、分解执行任务。

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/23154.html

(0)
上一篇 2021年7月20日
下一篇 2021年7月20日

相关推荐

发表回复

登录后才能评论