去往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()
}