柯理化

342 阅读5分钟

搞懂函数柯理化(currying)

vueresponse

一、什么是函数柯理化?

柯理化是一种转换方法,将可调用的函数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));

我们来解释,三个重要步骤:

  1. curry(sum)执行之后返回了一个函数curriedSum
  2. 然后执行curriedSum(1),a=1这个变量会被保存在词法作用域中,然后又返回一新的函数secondStep
  3. 执行secondStep(2),此时,第2步保存的变量a=1,和此时的2都会被传递给函数sum,返回最终结果3。

此时,你应该会想,一个简单的求两数之和搞这么复杂,不是有病么?接下来,我们就来解释下柯理化的目的。

二、柯理化的目的是什么?

​ 从一个故事说起,我有个需求,你能不能写一个函数,能返回两数之和,接到这个需求你说so easy,10秒就写完了这个代码,后来产品经理说,你这个函数能不能固定其中一个参数,比如1 + 21 + 31 + 4,你想了下,在不破坏sum函数的情况下,调用的时候,a这个参数传递固定值不就行了,你又花了1小时,把所有的地方都改了一遍。然后,产品经理说a这个参数客户要换成2,也就是2 + 22 + 3, 2 + 4,你想了下,我把调用的地方都改成一个常量不就行了,又是一顿操作,你把2都换成了一个全局常量。当然这个故事还没完,产品经理说,另外一个功能,要求3 + 23 + 33 +4,然后你又定义了一个全局常量。咳咳咳,又有一个功能4 + 24 + 34 + 4,你又定义了一个全局常量。咳咳咳,又有一个功能5 + 25 + 35 + 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));
      }
    }
  };
}

这里有个判断,判断传入参数的格式:

  1. 如果传入的参数args的个数大于或等于原函数func的参数,那么直接调用func函数,返回执行结果;

  2. 如果传入的参数args的个数小于原函数func的参数,就返回一个函数,这个函数保留了原来的参数args,下次执行的时候将传入的参数与原来的参数拼接在一起,再执行一次curried函数,在最后拼接的参数会满足条件1.

    为了加深理解,可以再看下小可爱的例子,运行观察这个console.log(curriedGetUserInfo('小可爱')('2')('闺女'))结果产生的执行步骤,观察每一步的执行过程。

四、总结

柯理化实现了

  • 逐步接受参数,并缓存供后期使用
  • 不立即计算执行,延后执行
  • 符合计算的条件,将缓存的参数,统一传递给执行的方法

穷则变,变则通,通则久(易经)