搞懂函数柯理化(currying)
一、什么是函数柯理化?
柯理化是一种转换方法,将可调用的函数f(a, b, c)转换为可调用的函数f(a)(b)(c)。
看完这个定义,如果你之前没有接触过这个概念,这句话就相当于没说,所以为了搞清楚这个概念,我们举一个例子🌰。这个例子就是求两数之和,不要小看这个例子🌰哦,简单的例子,往往透彻深奥的道理,越是伟大的东西,越是谦卑,请看下以下代码。
function curry(f) {
return function(a) { // 返回一个函数
return function (b) { // 咦,又返回一个函数
return f(a, b); // 最后,返回函数f真正执行的结果,整个curry函数就像一颗洋葱,被一层层拨开
}
}
}
function sum (a, b) { // 求两数之和
return a + b;
}
let curriedSum = curry(sum); // 第1步执行 扒了第1层 返回是一个函数
let secondStep = curriedSum(1); // 第2步执行 扒了第2层 此时在词法作用域中 保存了a = 1
let thirdStep = secondStep(2) // 第3步执行 扒了第3层 返回最终结果3
console.log('最终执行结果', thirdStep)
console.log('与一次性执行,返回结果一致', curry(sum)(1)(2));
我们来解释,三个重要步骤:
curry(sum)
执行之后返回了一个函数curriedSum
。- 然后执行
curriedSum(1)
,a=1这个变量会被保存在词法作用域中,然后又返回一新的函数secondStep
。 - 执行
secondStep(2)
,此时,第2步保存的变量a=1,和此时的2都会被传递给函数sum
,返回最终结果3。
此时,你应该会想,一个简单的求两数之和搞这么复杂,不是有病么?接下来,我们就来解释下柯理化的目的。
二、柯理化的目的是什么?
从一个故事说起,我有个需求,你能不能写一个函数,能返回两数之和,接到这个需求你说so easy,10秒就写完了这个代码,后来产品经理说,你这个函数能不能固定其中一个参数,比如1 + 2
,1 + 3
,1 + 4
,你想了下,在不破坏sum
函数的情况下,调用的时候,a这个参数传递固定值不就行了,你又花了1小时,把所有的地方都改了一遍。然后,产品经理说a这个参数客户要换成2,也就是2 + 2
,2 + 3
, 2 + 4
,你想了下,我把调用的地方都改成一个常量不就行了,又是一顿操作,你把2都换成了一个全局常量。当然这个故事还没完,产品经理说,另外一个功能,要求3 + 2
,3 + 3
,3 +4
,然后你又定义了一个全局常量。咳咳咳,又有一个功能4 + 2
,4 + 3
,4 + 4
,你又定义了一个全局常量。咳咳咳,又有一个功能5 + 2
,5 + 3
,5 + 4
,你又定义了一个全局常量。这显示不是老司机的套路,老司机的套路是更好的应对变化,封装变化的部分。回过头来,看篇章一,我们使用let curriedSum = curry(sum);
去创建方法curriedSum(2);curriedSum(3);curriedSum(4)
,是不是可以更好地解决产品经理提出的需求。我们下面再举一个活生生的例子🌰。
写一个打印日志的功能,包含时间time、日志等级importance(等级包括INFO/WARNING/ERROR)、消息详情message,就像下面这样。
# 三条日志,时间相同,等级不同,内容不同
2020-11-4-22:52:38-INFO-requestOK
2020-11-4-22:52:38-WARNING-requestBad
2020-11-4-22:52:38-Error-request500
# 三条日志,时间相同,等级相同,内容不同
2020-11-4-22:52:38-INFO-getNameOK
2020-11-4-22:52:38-INFO-getAgeOK
2020-11-4-22:52:38-INFO-getSexOK
可以思考下用柯理化的思想怎么实现这个功能。这里我们给出一种实现。
function log(date, importance, message) {
let time = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}- ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`
console.log(`${time}-${importance}-${message}`);
}
function curry(func) { // 柯理化关键代码
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
}
}
};
}
let currTimeLog = curry(log)(new Date) // 生成了一个函数,可以打印时间相同,等级不同,内容不同的日志
currTimeLog('INFO', 'requestOK')
currTimeLog('WARNING', 'requestBad')
currTimeLog('Error', 'request500')
let currTimeINFOLog = currTimeLog('INFO') // 生成了一个函数,可以打印时间相同,等级相同,内容不同的日志
currTimeINFOLog('getNameOK')
currTimeINFOLog('getAgeOK')
currTimeINFOLog('getSexOK')
这段代码最关键的地方是curry
函数,下面我们就来详细解释下这段代码,也是常考手写代码之一。
三、高级柯理化函数curry的实现
之所以说高级,在于这个方法可以支持单参数顺序执行与多参数一次执行,举个活生生(real-life)的可爱的例子🌰。
function getUserInfo(name, age, sex) {
return `${name}-${age}-${sex};`
}
let curriedGetUserInfo = curry(getUserInfo);
console.log(curriedGetUserInfo('小可爱', '2', '闺女')); // 6,仍然可以被正常调用
console.log(curriedGetUserInfo('小可爱')('2','闺女')); // 6,对第一个参数的柯里化
console.log(curriedGetUserInfo('小可爱')('2')('闺女')); // 6,全柯里化
运行这个例子,然后我们剖析下curry
函数。
也就是以下这段代码,希望你理解它,甚至可以手写这段代码
function curry(func) { // 柯理化关键代码
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
}
}
};
}
这里有个判断,判断传入参数的格式:
-
如果传入的参数
args
的个数大于或等于原函数func
的参数,那么直接调用func
函数,返回执行结果; -
如果传入的参数
args
的个数小于原函数func
的参数,就返回一个函数,这个函数保留了原来的参数args
,下次执行的时候将传入的参数与原来的参数拼接在一起,再执行一次curried
函数,在最后拼接的参数会满足条件1.为了加深理解,可以再看下小可爱的例子,运行观察这个
console.log(curriedGetUserInfo('小可爱')('2')('闺女'))
结果产生的执行步骤,观察每一步的执行过程。
四、总结
柯理化实现了
- 逐步接受参数,并缓存供后期使用
- 不立即计算执行,延后执行
- 符合计算的条件,将缓存的参数,统一传递给执行的方法
穷则变,变则通,通则久(易经)