JS 函数式编程思维简述(三):柯里化
- 简述
- 无副作用(No Side Effects)
- 高阶函数(High-Order Function)
- 柯里化(Currying)
- 闭包(Closure)
- 不可变(Immutable)
- 惰性计算(Lazy Evaluation)
- 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
的一个函数,分解为参数数量为 1
的 n
个函数,并且支持连续调用。例如:
// 一个用于计算三个数字累加的函数
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
} );