去往js函数式编程(6)

一起柯里化

  柯里化是将 m 元函数转换为一系列 m 个一元函数的过程,其中每个函数从左到右接收原始函数的一个参数。(第一个函数接收原始函数的第一个参数,并返回一个接收第二个参数的第二个函数,依此类推。)每个函数在调用时生成序列中的下一个函数,最后一个函数执行实际的计算。

  柯里化的思想本身很简单。如果你有一个带三个参数的方法,你可以用箭头函数编写成这样。const make3 =(a,b,c)=>String(100a+10b+c);你可以创建成一系列带有单个参数的函数:const make3curried=a=>b=>c=>String(100a_10b+c).写成嵌套函数:

const make3curried2 = function (a) {
  return function (b) {
    return function (c) {
      return String(100 * a + 10 * b + c)
    }
  }
}

  在使用上,每个函数的用法有一个重要的区别。第一个函数可以按照通常的方式进行调用,例如 make3(1,2,3),但是第二个定义,这样的调用方式不起作用。正确的调用方式是 make3curried(1)(2)(3).

  我们在看下 make3 和 make3curried 之间的主要区别:make3 是一个三元函数,而 make3curried 是一元函数。make3 返回一个字符串;make3curried 返回另一个函数,这个函数又返回一个第二个函数,最后返回一个字符串。你调用 make3(1,2,3)和 make3curried(1)(2)(3)都会获得相同的 123.

为什么要费这么大劲呢?

  我们看一个简单的例子,假如你有一个计算增值税的函数。

const addVAT = (rate, amount) => amount * (1 + rate / 100)

addVAT(20, 500) // 600 500+20%
addVAT(15, 200) // 230 200+15%

  如果你需要一个固定的税率,那么可以对 addVAT 函数进行柯里化,生成一个始终给定税率的更专门的版本。

const addVATcurried = (rate) => (amount) => amount * (1 + rate / 100)
const addNationVAT = addVATcurried(6)
addNationVAT(1500) // 1590 1500+6%

  你也许会说,为了固定一个 6%的税进行柯里化是不是复杂了。我们再看一个例子。你计划在程序中添加一些日志记录。

let myLog = (severity, logText) => {
  // 根据严重性(n,w,e)显示logText
}

  如果你使用这种方法,每当你想要显示普通的日志消息时,编写 myLog('Normal','bla'),警告要写成 myLog('WARNING','警告'),通过柯里化,你可以固定 myLog()的第一个参数。

// 当作柯里化了
myLog = curry(myLog)

const myNormalLog = myLog('NORMAL')
const myWarningLog = myLog('WARNING')
const myErrorLog = myLog('ERROR')

  这样的话,你只需要在不同的情况下调用 myNormalLog 或 myWarningLog 就可以了。我们接下来实现 curry 方法。


  我们看一些自动完成柯里化的方法,这样我们就可以生成任何函数的等效柯里化版本,甚至无需预先知道它的参数个数。例如,我们有一个 sun(x,y)函数。

sum(3, 5) //8
const add3 = sum(3)
add3(5) //8

sum(3)(5) //8

const sum=(x,y)=>{
  if(x!+=undefined && y!==undefined){
    return x+y
  }else if(x!==undefined && y===undefined){
    return z=>sum(x,z)
  }else {
    return sum
  }
}

  如果我们用两个参数调用它,它会将它们相加并返回和;如果只提供一个参数,它会返回一个新的函数。新函数期望一个参数,并返回该参数与原始参数的和。最后我们没有提供任何参数,它会返回自身。

  我们可以通过使用 bind()方法来找到柯里化的解决方案。这样可以让我们固定一个参数,并提供一个具有该固定参数的函数。

const curryByBind = (fn) =>
  fn.length === 0 ? fn() : (p) => curryByBind(fn.bind(null, p))

  使用箭头函数手动进行部分应用,就像我们使用柯里化一样,会变得过于复杂。使用箭头函数进行部分应用要简单很多。

const nonsense = (a, b, c, d, e) => `${a}/${b}/${c}/${d}/${e}`
const fix2and5 = (a, c, d) => nonsense(a, 22, c, d, 1960)

const fixLast = (a, c) => fix2and5(a, c, 9)

  如果你想写成 nonsense(a,22,c,9,1960),也可以,但事实仍然是使用箭头函数固定参数是简单的。我们考虑一个更通用的解决方案。

  我们可以使用部分柯里化。假设我们有了一个 partialCurry()函数。

const nonsense = (a, b, c, d, e) => `${a}/${b}/${c}/${d}/${e}`

const pcNonsense = partialCurry(nonsense)
const fix1And2 = pcNonsense(9, 22) // fix1And2现在是一个三元函数
const fix3 = fix1And2(60) // fix3时一个二元函数
const fix4And5 = fix3(12, 4) //fix4and5===nonsense(9,22,60,12,4)

  部分柯里化与柯里化和部分应用有一些共同点,但也有一些区别:原始函数被转化为一系列函数,每个函数产生下一函数,直到系列中的最后一个函数执行实际的计算。始终从第一个参数开始提供参数,就像柯里化一样,但可以提供多个参数,就像部分应用一样。当对函数进行柯里化时,所有中间函数都是一元函数,但在部分柯里化中不一定如此。

const partialCurryByClosure = (fn) => {
  const curryize =
    (...args1) =>
    (...args2) => {
      const allParams = [...args1, ...args2]
      return (allParams.length < func, length ? curryize : fn)(...allParams)
    }
  return curryize()
}

// 如果要处理参数个数不固定的函数,可以给它提供一个额外的参数:
const partialCurryByClosure2 = (fn, len = fn.length) => {
  const curryize =
    (...args1) =>
    (...args2) => {
      const allParams = [...args1, ...args2]
      return (allParams.length < len ? curryize : fn)(...allParams)
    }
  return curryize()
}
posted @ 2023-06-13 17:34  艾路  阅读(9)  评论(0编辑  收藏  举报