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

  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);
...
JS 函数式编程思维简述(三):柯里化_第1张图片
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 函数式编程思维简述(三):柯里化)