函数式编程中部分运用(Partial Application)和部分套用(Currying)的区分

部份运用(Partial Application,也译作“偏运用”或“部份运用”)和部份
套用( Currying, 也译作“柯里化”),是函数式编程范式中很经常运用的技能。
本文着重于论述它们的特性和(更主要的是)差异。

元(arity)

在后续的代码示例中,会频仍涌现 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 罢了,不
过类似的运用场景照样能够设想取得的。那末这个例子和部份运用有什么关联呢?

以下是一些客观陈说的实际(然则很主要,确保你看邃晓了):

  1. 我们的 map 是一个二元函数;
  2. square 是一个一元函数;
  3. 挪用我们的 map 时,我们传入了两个参数([2, 3, 5]square),
    这两个参数都运用在 map 函数里,并返回给我们终究的效果。

简单明了吧?由于 map 要两个参数,我们也给了两个参数,因而我们能够说:

map 函数 完整运用 了我们传入的参数。

而所谓部份运用就像它的字面意义一样,函数挪用的时刻只供应部份参数供其运用
——比方说上例,挪用 map 的时刻只传给它一个参数。

但是这要怎样完成呢?

起首,我们把 map 包装一下:

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 行的目标是为了传入用于最
终处置惩罚的函数。因而我们须要先传入举行终究处置惩罚的函数,然后再给它分批传入参
数(部份运用),以取得更大的运用灵活性。

回过头来解读一下这两个名词:

  • 部份运用: 返回终究效果的处置惩罚方式是限制的,每一层的函数挪用所传入
    的参数都将逐次介入终究处置惩罚历程中去;
  • 部份套用: 返回终究效果的处置惩罚方式是未知的,须要我们在运用的时刻将
    其作为参数传入。

最左情势(leftmost)与最右情势(rightmost)的部份套用

在前面的例子中,为何要把部份套用函数定名为 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),最左情势和最右情势会对运用形状的语义化表达发生差别的影
响:

  1. 关于最右情势的运用,如 squareAll([...]),它的潜台词是:不论传入
    的是什么,把它们挨个都平方咯。从语义角度来看,square 是主体,而
    传入的数组是客体;
  2. 关于最左情势的运用,如 oneToThreeEach(...),没必要说,天然是之前传入
    [1, 2, 3] 是主体,而以后传入的 squaredouble 才是客体;

所以说,依据运用的场景来挑选最合适的情势吧,没必要拘泥于特定的某种情势。

回到实际

至此,我们已把部份运用和部份套用的玄妙差异剖析的透辟了,但这更多的是理
论性子的研讨罢了,实际中这两者的界线则异常隐约——所以许多人习气等量齐观
也就不很不测了。

就拿 rightmostCurry 谁人例子来讲吧:

function rightmostCurry(binaryFn) {
  return function (secondArg) {
    return function (firstArg) {
      return binaryFn(firstArg, secondArg);
    };
  };
}

像如许部份套用搀杂着部份运用的代码在实际中只能算是“半成品”,为何呢?
由于你很快会发明如许的为难:

var squareAll = rightmostCurry(map)(square);
var doubleAll = rightmostCurry(map)(double);

像如许的“先部份套用然后紧接着部份运用”的形式是异常广泛的,我们为何不
进一步笼统化它呢?

关于广泛化的形式,人们习气于给它一个定名。关于上面的例子,可分解形貌为:

  1. 最右情势的部份套用
  2. 针对 map
  3. 一元
  4. 部份运用

理一理语序能够组合成:针对 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

予我的协助和指点是我难以遗忘的,在此向两位以及一切协助我的大牛们申谢!

    原文作者:n͛i͛g͛h͛t͛i͛r͛e͛
    原文地址: https://segmentfault.com/a/1190000000765247
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞