局部应用(Partial Application,也译做“偏应用”或“部分应用”)和局部
套用( Currying, 也译做“柯里化”),是函数式编程范式中很经常使用的技巧。
本文着重于阐述它们的特色和(更重要的是)差别。编程
在后续的代码示例中,会频繁出现 unary(一元),binary(二元),
ternary(三元)或 polyadic(多元,即多于一元)以及 variadic(可变
元)等数学用语。在本文所表述的范围内,它们都是用来描述函数的参数数量的。数组
先来一个“无聊”的例子,实现一个 map
函数:数据结构
function map(list, unaryFn) { return [].map.call(list, unaryFn); } function square(n) { return n * n; } map([2, 3, 5], square); // => [4, 9, 25]
这个例子固然缺少实用价值,咱们仅仅是仿造了数组的原型方法 map
而已,不
过相似的应用场景仍是能够想象获得的。那么这个例子和局部应用有什么关联呢?app
如下是一些客观陈述的事实(可是很重要,确保你看明白了):函数式编程
map
是一个二元函数;square
是一个一元函数;map
时,咱们传入了两个参数([2, 3, 5]
和 square
),map
函数里,并返回给咱们最终的结果。简单明了吧?因为 map
要两个参数,咱们也给了两个参数,因而咱们能够说:函数
map
函数 彻底应用 了咱们传入的参数。code
而所谓局部应用就像它的字面意思同样,函数调用的时候只提供部分参数供其应用
——比方说上例,调用 map
的时候只传给它一个参数。htm
但是这要怎么实现呢?对象
首先,咱们把 map
包装一下:ip
function mapWith(list, unaryFn) { return map(list, unaryFn); }
而后,咱们把二元的包装函数变成两个层叠的一元函数:
function mapWith(unaryFn) { return function (list) { return map(list, unaryFn); }; }
因而,这个包装函数就变成了:先接收一个参数,而后返回给咱们一个函数来接受
第二个参数,最终再返回结果。也就是这样:
mapWith(square)([2, 3, 5]); // => [4, 9, 25]
到目前为止,局部应用彷佛没有体现出什么特别的价值,然而若是咱们把应用场景
稍微扩展一下的话……
var squareAll = mapWith(square); squareAll([2, 3, 5]); // => [4, 9, 25] squareAll([1, 4, 7, 6]); // => [1, 16, 49, 36]
咱们把对象 square
(函数即对象)做为部分参数应用在 map
函数中,获得一
个一元函数,即 squareAll
,因而咱们能够想怎么用就怎么用。这就是局部应用
,恰当的使用这个技巧会很是有用。
咱们能够在局部应用的例子的基础上继续探索局部套用,首先把前面的 mapWith
稍微修整修整:
function wrapper(unaryFn) { return function(list) { return binaryFn(list, unaryFn); }; }
function wrapper(secondArg) { return function(firstArg) { return binaryFn(firstArg, secondArg); }; }
如上,我刻意把修整分做两步来写。第一步,咱们把 map
用一个更抽象的binaryFn
取代,暗示咱们不局限于作数组映射,能够是任何一种二元函数的处
理;同时,最外层的 mapWith
也就没有必要了,使用更抽象的 wrapper
取代
。第二步,既然用做处理的函数都抽象化了,传入的参数天然也没有必要限定其类
型,因而就获得了最终的形态。
接下来的思考很是关键,请跟紧咯!
考虑一下未修整前的形态,最里层的 map
是哪里来的?——那是咱们在最开始
的时候本身定义的。然而到了修整后的形态,binaryFn
是个抽象的概念,此时
此刻咱们并无对应的函数能够直接调用它,那么咱们要如何提供这一步?
再包装一层,把 binaryFn
做为参数传进来——
1 function rightmostCurry(binaryFn) { 2 return function (secondArg) { 3 return function (firstArg) { 4 return binaryFn(firstArg, secondArg); 5 }; 6 }; 7 }
你是否意识到这其实就是函数式编程的本质(的体现形式之一)?
那么,局部套用是如何体现出来的呢?咱们把一开始写的那个 map
函数套用进
来玩玩:
var rightmostCurriedMap = rightmostCurry(map); var squareAll = rightmostCurriedMap(square); squareAll([2, 3, 5]); // => [4, 9, 25] squareAll([1, 4, 7, 6]); // => [1, 16, 49, 36]
最后三句和以前讲局部应用的例子是同样的,局部套用的体现就在第一句上。乍一
看,这貌似就是又多了一层局部应用而已啊?不,它们是有差异的!
对比一下两个例子:
// 局部应用 function mapWith(unaryFn) { return function (list) { return map(list, unaryFn); }; } // 局部套用 1 function rightmostCurry(binaryFn) { 2 return function (secondArg) { 3 return function (firstArg) { 4 return binaryFn(firstArg, secondArg); 5 }; 6 }; 7 }
在局部应用的例子里,最内层的处理函数是肯定的;换言之,咱们对最终的处理方
式是有预期的。咱们只是把传入参数分批完成,以得到:一)较大的应用灵活性;
二)更单纯的函数调用形态。
而在局部套用的例子里,第 2~6
行仍是局部应用——这没差异;可是能够看出
最内层的处理在定义的时候实际上是未知的,而第 1
行的目的是为了传入用于最
终处理的函数。所以咱们须要先传入进行最终处理的函数,而后再给它分批传入参
数(局部应用),以得到更大的应用灵活性。
回过头来解读一下这两个名词:
在前面的例子中,为何要把局部套用函数命名为 rightmostCurry
?另外,是
否还有与之对应的 leftmostCurry
呢?
请回头再看一眼上例的第 2~6
行,会发现层叠的两个一元函数先传入secondArg
,再传入 firstArg
,而最内层的处理函数则是反过来的。如此一来
,咱们先接受最右边的,再接受最左边的,这就叫最右形式的局部套用;反之则是
最左形式的局部套用。
即便在本文的例子里都使用二元参数,但其实多元也是同样的,无非就是增长局
部应用的层叠数量;而可变元的应用也不难,彻底能够用某种数据结构来封装多
个元参数(如数组)而后再进行解构处理——ES6 的改进会让这一点变得更加简
单。
可是这又有什么实际意义呢?仔细对比下面两个代码示例:
function rightmostCurry(binaryFn) { return function (secondArg) { return function (firstArg) { return binaryFn(firstArg, secondArg); }; }; } var rightmostCurriedMap = rightmostCurry(map); function square(n) { return n * n; } var squareAll = rightmostCurriedMap(square); squareAll([2, 3, 5]); // => [4, 9, 25] squareAll([1, 4, 7, 6]); // => [1, 16, 49, 36]
function leftmostCurry(binaryFn) { return function (firstArg) { return function (secondArg) { return binaryFn(firstArg, secondArg); }; }; } var leftmostCurriedMap = leftmostCurry(map); function square(n) { return n * n; } function double(n) { return n + n; } var oneToThreeEach = leftmostCurriedMap([1, 2, 3]); oneToThreeEach(square); // => [1, 4, 9] oneToThreeEach(double); // => [2, 4, 6]
这两个例子很容易理解,我想就无须赘述了。值得注意的是,因为“从左向右”的
处理更合逻辑一些,因此现实中最左形式的局部套用比较常见,并且习惯上直接把
最左形式的局部套用就叫作 curry,因此若是没有显式的 rightmost 出现,
那么就能够按照惯例认为它是最左形式的。
最后,什么时候用最左形式什么时候用最右形式?嗯……这个其实没有规定的,彻底取决于
你的应用场景更适合用哪一种形式来表达。从上面的对比中能够发现一样的局部套用
(都套用 map
),最左形式和最右形式会对应用形态的语义化表达产生不一样的影
响:
squareAll([...])
,它的潜台词是:无论传入square
是主体,而oneToThreeEach(...)
,没必要说,天然是以前传入[1, 2, 3]
是主体,而以后传入的 square
或 double
才是客体;因此说,根据应用的场景来选择最合适的形式吧,没必要拘泥于特定的某种形式。
至此,咱们已经把局部应用和局部套用的微妙差异分析的透彻了,但这更多的是理
论性质的研究罢了,现实中这二者的界限则很是模糊——因此不少人习惯混为一谈
也就不很意外了。
就拿 rightmostCurry
那个例子来讲吧:
function rightmostCurry(binaryFn) { return function (secondArg) { return function (firstArg) { return binaryFn(firstArg, secondArg); }; }; }
像这样局部套用掺杂着局部应用的代码在现实中只能算是“半成品”,为何呢?
由于你很快会发现这样的尴尬:
var squareAll = rightmostCurry(map)(square); var doubleAll = rightmostCurry(map)(double);
像这样的“先局部套用而后紧接着局部应用”的模式是很是广泛的,咱们为何不
进一步抽象化它呢?
对于广泛化的模式,人们习惯于给它一个命名。对于上面的例子,可分解描述为:
map
理一理语序能够组合成:针对 map
的最右形式(局部套用)的一元局部应用。
真尼玛的啰嗦!
实际上咱们真正想作的是:先给 map
函数局部应用一个参数,返回的结果能够
继续应用 map
须要的另一个参数(固然,你能够把 map
替换成其余的函
数,这就是局部套用的职责表现了)。真正留给咱们要实现的仅仅是返回另一部
分用于局部应用的一元函数罢了。
所以按照函数式编程的习惯,rightmostCurry
能够简化成:
function rightmostUnaryPartialApplication(binaryFn, secondArg) { return rightmostCurry(binaryFn, secondArg); }
先别管冗长的命名,接着咱们套用局部应用的技巧,进一步改写成更简明易懂的形
式:
function rightmostUnaryPartialApplication(binaryFn, secondArg) { return function (firstArg) { return binaryFn(firstArg, secondArg); }; }
这才是你在现实中随处可见的“彻底形态”!至于冗长的命名,小问题啦:
var applyLast = rightmostUnaryPartialApplication; var squareAll = applyLast(map, square); var doubleAll = applyLast(map, double);
如此一来,最左形式的类似实现就能够无脑出炉了:
function applyFirst(binaryFn, firstArg) { return function (secondArg) { return binaryFn(firstArg, secondArg); }; }
其实这样的代码不少开发者都已经写过无数次了,但是若是你请教这是什么写法,
回答你“局部应用”或“局部套用”的都会有。对于初学者来讲就容易闹不清楚到
底有什么区别,长此以往就干脆认为是一回事儿了。不过如今你应该明白过来了,
这个彻底体实际上是“局部应用”和“局部套用”的综合应用。
各用一句话作个小结吧:
局部应用(Partial Application):是一种转换技巧,经过预先传入一个或多
个参数来把多元函数转变为更少一些元的函数甚或是一元函数。
局部套用(Currying):是一种解构技巧,用于把多元函数分解为多个可链式调
用的层叠式的一元函数,这种解构能够容许你在其中局部应用一个或多个参数,但
是局部套用自己不提供任何参数——它提供的是调用链里的最终处理函数。
后记:撰写本文的时间跨度较长,期间参考的资料和代码没法一一计数。可是
Raganwald 的书和博客 以及 Michael Fogue 的 Functional JavaScript 给 予个人帮助和指导是我难以忘记的,在此向两位以及全部帮助个人大牛们致谢!