美文网首页JS相关
JS 函数式编程思维简述(三):柯里化

JS 函数式编程思维简述(三):柯里化

作者: 阿拉拉布 | 来源:发表于2018-12-18 19:37 被阅读79次
    1. 简述
    2. 无副作用(No Side Effects)
    3. 高阶函数(High-Order Function)
    4. 柯里化(Currying)
    5. 闭包(Closure)
    6. 不可变(Immutable)
    7. 惰性计算(Lazy Evaluation)
    8. Monad

    偏函数(Partial Application)

           在探讨柯里化之前,我们首先聊一聊很容易跟其混淆的另一个概念——偏函数(Partial Application)。在维基百科中,对 Partial Application 的定义是这样的:

    In computer science, partial application (or partial function application) refers to the process of fixing a number of arguments to a function, producing another function of smaller arity.
    

    其含义是:在计算机科学中,局部应用(或偏函数应用)是指将多个参数固定在一个函数中,从而产生另一个函数的过程。

    举个例子,假设我们是一个加工厂,用于生产梯形的零件,生产过程中我们要根据订单来源方给的一系列参数计算面积:

    // 声明一个计算梯形面积的函数
    function trapezoidArea(a, b, h){
        return (a + b) * h / 2;
    }
    
    

    突然有一天,我们发现了一个问题:我们的大部分订单零件,都是高度为 28 的规格,此时面积函数调用经常是这个样子的:

    trapezoidArea(26, 38, 28);
    trapezoidArea(20, 40, 28);
    trapezoidArea(36, 58, 28);
    trapezoidArea(14, 19, 15);
    trapezoidArea(33, 35, 28);
    ...
    
    image

    此时,我们便可以以第一个函数为模板,来创建存储了固定值的新的计算函数

    // 声明一个固定高度的梯形面积计算函数
    function trapezoidAreaByHeight28(a, b){
        return trapezoidArea(a, b, 28);
    }
    
    trapezoidAreaByHeight28(3, 6); // 结果: 126
    

    当然,这个示例中并没有以明显的 偏函数 的方式去呈现,我们可以让返回结果变成一个新的函数,因此我们可以加以改造:

    // 声明一个计算梯形面积的函数
    function trapezoidArea(a, b, h){
        return (a + b) * h / 2;
    }
    // 声明一个【可以生成固定高度的梯形面积计算】的工厂函数
    function trapezoidFactory(h){
        return function(a, b){ 
            return trapezoidArea(a, b, h);
        }
    }
    
    // 通过 trapezoidArea() 函数,生成绑定了固定参数的新的函数
    const trapezoidAreaByHeight15 = trapezoidFactory(15);
    const trapezoidAreaByHeight28 = trapezoidFactory(28);
    
    trapezoidAreaByHeight15(6, 13); // 结果: 142.5
    trapezoidAreaByHeight28(6, 13); // 结果: 266
    

    也可以将其简化为:

    const trapezoidAreaByHeight33 = (a, b) => trapezoidArea.call(null, a, b, 33);
    trapezoidAreaByHeight33(6, 13); // 结果: 313.5
    

    这里,我们就可以将 trapezoidAreaByHeight15()trapezoidAreaByHeight28()trapezoidAreaByHeight33() 视为 trapezoidArea() 的偏函数。

    偏函数的应用

           偏函数往往不能改变一个函数的行为,通常是根据一个已有函数而生成一个新的函数,这个新的函数具有已有函数的相同功能,区别在于在新的函数中有一些参数已被固定不会变更。偏函数的设计通常:

    • 减少了参数相似性高的函数调用过程;
    • 降低了函数的通用性,提高了函数的适用性,使其更专注于做某事;
    • 减少了程序耦合度,提高了专有函数的可维护性。

    柯里化(Currying)

           柯里化(Currying)是以美国数理逻辑学家哈斯凯尔·科里(Haskell Curry)的名字命名的函数应用方式。与偏函数很像的地方是:都可以缓存参数,都会返回一个新的函数,以提高程序中函数的适用性。而不同点在于,柯里化(Currying)通常用于分解原函数式,将参数数量为 n 的一个函数,分解为参数数量为 1n 个函数,并且支持连续调用。例如:

    // 一个用于计算三个数字累加的函数
    const addExample = function(a, b, c){
        return a + b + c;
    }
    // 调用
    addExample(10, 5, 3); // 结果: 18
    
    // 通过柯里化,对上述函数进行演变
    const addCurry = function(a){
        return function(b){
            return function(c){
                return a + b + c;
            }
        }
    }
    // 缔造新的 单一元 函数
    const add10 = addCurry(10);
    const add15 = add10(5);
    const add18 = add15(3);
    // 调用
    add18();    // 结果: 18
    

    可见,柯里化(Currying)用于将多元任务分解成单一任务,每一个独立的任务都缓存了上一次函数生成时传递的入参,并且让新生成的函数更简单、专注。上述演变也可以写作:

    // 通过ES6箭头函数构造将更加简单
    const addCurry = (a) => (b) => (c) => a + b + c;
    
    // 调用也可以这样
    addCurry(10)(5)(3); // 结果: 18
    

    柯里化的应用

           柯里化(Currying)分解了函数设计过程,将运行的步骤拆分为每一个单一参数的 lambda 演算。这里例举一个在 JavaScript 中用于做强制类型判断的示例:

    // 创建一个用于检测数据类型的函数 checkType() 
    const checkType = (e, typeStr) => Object.prototype.toString.call(e) === '[object '+typeStr+']';
    
    // 调用示范
    checkType(12, 'Number');            // 结果:true
    checkType(16.8, 'Number');          // 结果:true
    checkType(NaN, 'Number');           // 结果:true
    checkType(Infinity, 'Number');      // 结果:true
    checkType('abc', 'String');         // 结果:true
    checkType(true, 'Boolean');         // 结果:true
    checkType({}, 'Object');            // 结果:true
    checkType([], 'Array');             // 结果:true
    checkType(null, 'Null');            // 结果:true
    checkType(undefined, 'Undefined');  // 结果:true
    checkType(checkType, 'Function');   // 结果:true
    checkType(Symbol(), 'Symbol');      // 结果:true
    

    使用这一的方式构建的函数 checkType() 具备了高通用性,但适用性则略差。我们发现每次的调用过程,使用者都需要编写参数 typeStr 表示的类型字符串,增加了函数的应用复杂度。此时作为设计者,就可以对该函数加以改造,使其生成多个具备高适用性的独立函数:

    // 检测值是否是 Number
    const isNumber = (e) => checkType(e, 'Number');
    // 检测值是否是 String
    const isString = (e) => checkType(e, 'String');
    // 检测值是否是 Boolean
    const isBoolean = (e) => checkType(e, 'Boolean');
    // 检测值是否是 Object
    const isObject = (e) => checkType(e, 'Object');
    // 检测值是否是 Array
    const isArray = (e) => checkType(e, 'Array');
    // 检测值是否是 Null
    const isNull = (e) => checkType(e, 'Null');
    // 检测值是否是 Undefined
    const isUndefined = (e) => checkType(e, 'Undefined');
    // 检测值是否是 Function
    const isFunction = (e) => checkType(e, 'Function');
    // 检测值是否是 Symbol
    const isSymbol = (e) => checkType(e, 'Symbol');
    

    柯里化无限调用

           柯里化(Currying)分解了函数设计过程,将运行的步骤拆分为每一个单一参数的 lambda 演算。我们可以通过递归的方式,来构造出一个可进行无限调用,并返回相同的累加函数的 柯里化函数

    // 一个永远累加的函数,返回结果的新函数中缓存上一次调用,并进行数据累加
    // 最终的数据依赖 success 回调函数获取
    const alwaysAdd = function f1(nexter1){
        const n1 = nexter1;
        typeof n1.success == 'function' && n1.success(n1.value);
        return function f2(nexter2){
            const n2 = nexter2;
            return f1( {value: n1.value+n2.value, success: n2.success} );
        }
    }
    

    调用方式如:

    const r1 = alwaysAdd({value: 2});
    const r2 = r1({value: 5});
    const r3 = r2({value:4, success: (result)=>console.log('结果: ', result)});
    

    以这样的方式,我们构建的参数是一个简单对象 nexter,该对象至少包含一个 value 属性,用于描述本次累加的值。如果希望获取累加结果,则为 nexter 对象赋予函数属性 success 即可。结果会以实参的形式,传递给 success 函数用于传递通知。

    一个简单的 Promise

           Promise 对象无论是构造函数还是后续的链式调用中,都能看到柯里化设计的影子:接收单一参数,返回一个 Promise

    // 构建一个超级简单的 Promise 结构
    class MyPromise{
        constructor(executor) {
            this.value = null;
            typeof executor == 'function' && executor(this.resolve.bind(this), this.reject.bind(this));
        }
                    
        then(success){
            const result = success(this.value);
            const mp = new MyPromise();
            mp.value = result;
            return mp;
        }
                    
        resolve(value){
            this.value = value;
        }
                    
        reject(err){
            this.err = err;
        }
    }
    

    调用方式为:

    // 构建一个 MyPromise 对象
    const mp1 = new MyPromise((resolve, reject) => {
        resolve(10);
    });
    
    // 链式调用求值
    mp1.then( r => {
        console.log('mp1 r => ', r);    // 结果: 10
        return r + 3;
    } ).then( r => {
        console.log('mp2 r => ', r);    // 结果: 13
        return r + 5;
    } ).then( r => {
        console.log('mp3 r => ', r);    // 结果: 18
    } );
    

    相关文章

      网友评论

        本文标题:JS 函数式编程思维简述(三):柯里化

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