局部套用 和部分应用 是来源于数学的语言技术(基于 20 世纪数学家 Haskell Curry 和其余人的工做成果)。这两种技术存在于各类类型的语言中,能够单独或同时存在于函数式语言中。局部套用和部分应用使您可以处理函数或方法的参数数量,一般的方法是为一些参数提供一个或多个默认值(称为修正 参数)。全部 Java 下一代语言都包括局部套用和部分应用,但以不一样的方式实现它们。在本文中,我将介绍这两种技术的不一样之处,并展现它们在 Scala、Groovy 和 Clojure 中的实现细节,以及实际应用。html
出于本部分的目的,方法(method) 和 函数(function) 是可互换的。支持局部套用和部分应用的面向对象语言使用方法。一样,函数参数(function parameter) 和 函数参数(function argument) 也是可互换的。因为这些概念起源于数学,所以我自始至终使用的是 函数(function) 和 参数(argument),但这并不意味着这两种技术对方法不起做用。java
对于业余人士来讲,局部套用和部分应用具备相同的效果。使用这两种技术时,均可以建立一个一些参数具备预先提供值的函数版本:算法
使用局部套用和部分应用,能够提供参数值并返回一个可以使用缺乏参数调用的函数。可是,对函数应用局部套用会返回链中的下一个函数,而部分应用会将参数值绑到在运算期间提供的值上,生成一个具备更少 元数(参数的数量)的函数。当考虑具备两个以上元数的函数时,这一区别会更加明显。例如,process(x, y, z)
函数的彻底套用版本是process(x)(y)(z)
,其中 process(x)
和 process(x)(y)
都是接受一个参数的函数。若是只对第一个参数应用了局部套用,那么 process(x)
的返回值将是接受一个参数的函数,所以仅接受一个参数。与此相反,在使用部分应用时,会剩下一个具备更少元数的函数。对 process(x, y, z)
的一个参数使用部分应用会生成接受两个参数的函数:process(y, z)
。shell
这两种技术的结果一般是相同的,但两者的区别也很重要,人们一般会对它们之间的区别产生误解。更复杂的是,Groovy 能够实现部分应用和局部套用,但都将它们称为 currying
。而 Scala 具备偏应用函数(partially applied function)和 PartialFunction
,尽管它们的名称相似,但它们倒是两个不一样的概念。编程
Scala 支持局部套用和部分应用,还支持特征(trait),特征能够定义约束函数(constrained function)。设计模式
在 Scala 中,函数能够将多个参数列表定义为括号组。调用参数数量比其定义数量少的函数时,会返回一个将缺乏参数列表做为其参数的函数。请考虑 Scala 文档的示例,如清单 1 所示。数组
def filter(xs: List[Int], p: Int => Boolean): List[Int] = if (xs.isEmpty) xs else if (p(xs.head)) xs.head :: filter(xs.tail, p) else filter(xs.tail, p) def modN(n: Int)(x: Int) = ((x % n) == 0) val nums = List(1, 2, 3, 4, 5, 6, 7, 8) println(filter(nums, modN(2))) println(filter(nums, modN(3)))
在清单 1 中,filter()
函数递归地应用传递的过滤条件。modN()
函数定义了两个参数列表。在我使用 filter()
调用 modN
时,我传递了一个参数。filter()
函数被做为函数的第二个参数,具备一个 Int
参数和一个 Boolean
返回值,这与我传递的局部套用函数的签名相匹配。闭包
在 Scala 中还能够部分应用函数,如清单 2 所示。app
def price(product : String) : Double = product match { case "apples" => 140 case "oranges" => 223 } def withTax(cost: Double, state: String) : Double = state match { case "NY" => cost * 2 case "FL" => cost * 3 } val locallyTaxed = withTax(_: Double, "NY") val costOfApples = locallyTaxed(price("apples")) assert(Math.round(costOfApples) == 280)
在清单 2 中,我首先建立了一个 price
函数,它返回了产品和价格之间的映射。而后我建立了一个 withTax()
函数,其参数为 cost
和state
。可是,在特殊的源文件中,我知道要专门处理一个国家的税收。我没有对每次调用的额外参数应用局部套用,而是部分应用了 state
参数,并返回一个 state 值固定的函数。locallyTaxed
函数接受一个参数,即 cost
。框架
Scala PartialFunction
特征能够与模式无缝地配合使用(请阅读函数式思惟 系列的 "Either 树和模式匹配" 部分中的模式匹配)。尽管名称相似,但此特征不会建立偏应用函数。相反,可使用它定义仅适用于值和类型定义子集的函数。
Case 块是应用偏函数(partial function)的一种方式。清单 3 使用了 Scala 的 case
,没有传统对应的 match
操做符。
case
val cities = Map("Atlanta" -> "GA", "New York" -> "New York", "Chicago" -> "IL", "San Francsico " -> "CA", "Dallas" -> "TX") cities map { case (k, v) => println(k + " -> " + v) }
在清单 3 中,我建立了一个城市和该城市所对应的州的映射。而后,我对该集合调用了 map
函数,map
会拆开键值对以输出它们。在 Scala 中,包含 case
声明的代码块是定义匿名函数的一种方式。不使用 case
能够更简洁地定义匿名函数,可是,case
语法提供了如清单 4 所示的额外好处。
map
和 collect
之间的区别List(1, 3, 5, "seven") map { case i: Int ? i + 1 } // won't work // scala.MatchError: seven (of class java.lang.String) List(1, 3, 5, "seven") collect { case i: Int ? i + 1 } // verify assert(List(2, 4, 6) == (List(1, 3, 5, "seven") collect { case i: Int ? i + 1 }))
在清单 4 中,我不能在具备 case
的异构集合上使用 map
:我收到了 MatchError
,由于函数试图增长 seven
字符串。可是 collect
工做正常。为何会出现这种不一样?什么地方出错了?
Case 块定义的是偏函数,而不是偏应用函数。偏函数 具备有限的容许值。例如,数学函数 1/x
是无效的,若是 x = 0
。偏函数提供了一种定义容许值约束的方式。在 清单 4 的 collect
示例中,定义了 Int
而不是 String
的约束,所以没有收集 seven
字符串。
要定义偏函数,还可使用 PartialFunction
特征,如清单 5 所示。
val answerUnits = new PartialFunction[Int, Int] { def apply(d: Int) = 42 / d def isDefinedAt(d: Int) = d != 0 } assert(answerUnits.isDefinedAt(42)) assert(! answerUnits.isDefinedAt(0)) assert(answerUnits(42) == 1) //answerUnits(0) //java.lang.ArithmeticException: / by zero
在清单 5 中,我从 PartialFunction
特征导出了 answerUnits
,并提供了两个函数:apply()
和 isDefinedAt()
。apply()
函数计算值。我使用了 isDefinedAt()
(PartialFunction
的必要方法)来定义肯定参数适用性的约束。
还可使用 case
块实现偏函数,清单 5 的answerUnits
能够采用更简洁的方式编写,如清单 6 所示。
answerUnits
的另外一种定义def pAnswerUnits: PartialFunction[Int, Int] = { case d: Int if d != 0 => 42 / d } assert(pAnswerUnits(42) == 1) //pAnswerUnits(0) //scala.MatchError: 0 (of class java.lang.Integer)
在清单 6 中,我结合使用了 case
和保卫条件来约束值并同时提供值。与 清单 5 的一个明显区别是 MatchError
(而不是ArithmeticException
),由于清单 6 使用了模式匹配。
偏函数并不只局限于数值类型。它可使用全部类型的数值,包括 Any
。能够考虑增量器(incrementer)的实现,如清单 7 所示。
def inc: PartialFunction[Any, Int] = { case i: Int => i + 1 } assert(inc(41) == 42) //inc("Forty-one") //scala.MatchError: Forty-one (of class java.lang.String) assert(inc.isDefinedAt(41)) assert(! inc.isDefinedAt("Forty-one")) assert(List(42) == (List(41, "cat") collect inc))
在清单 7 中,我定义了一个偏函数来接受任意类型的输入 (Any
),但选择对类型子集作出反应。请注意,我还能够调用偏函数的isDefinedAt()
函数。使用 case
的 PartialFunction
特征的实现者能够调用 isDefinedAt()
,它是隐式定义的。在 清单 4 中,我说明了 map
和 collect
的表现不一样。偏函数的行为解释了它们的区别:collect
旨在接受偏函数,并调用元素的 isDefinedAt()
函数,会忽略那些不匹配的函数。
在 Scala 中,偏函数和偏应用函数的名称相似,可是它们提供了不一样的正交特性集。例如,没有什么能够阻止您部分地应用偏函数。
在个人函数式思惟 系列的 "运用函数式思惟,第 3 部分" 中详细介绍了 Groovy 中的局部套用和部分应用。Groovy 经过 curry()
函数实现了局部套用,该函数来自 Closure
类。尽管名称如此,但 curry()
实际上经过处理其下面的闭包块来实现部分应用。可是,您能够模拟局部套用,方法是使用部分应用将函数减小为一系列部分应用的单参数函数,如清单 8 所示。
def volume = { h, w, l -> return h * w * l } def area = volume.curry(1) def lengthPA = volume.curry(1, 1) //partial application def lengthC = volume.curry(1).curry(1) // currying println "The volume of the 2x3x4 rectangular solid is ${volume(2, 3, 4)}" println "The area of the 3x4 rectangle is ${area(3, 4)}" println "The length of the 6 line is ${lengthPA(6)}" println "The length of the 6 line via curried function is ${lengthC(6)}"
在清单 8 中,在两种 length
状况下,我使用 curry()
函数部分应用了参数。可是,在使用 lengthC
时,经过部分地应用参数,直到出现一连串的单参数函数为止,我制造了一种使用局部套用的幻觉。
Clojure 包含 (partial f a1 a2 ...)
函数,它具备函数 f
以及比所需数量更少的参数,并且返回一个在提供剩余参数时调用的部分应用函数。清单 9 显示了两个示例。
(def subtract-from-hundred (partial - 100)) (subtract-from-hundred 10) ; same as (- 100 10) ; 90 (subtract-from-hundred 10 20) ; same as (- 100 10 20) ; 70
在清单 9 中,我将 subtract-from-hundred
函数定义为部分应用的 -
运算符(Clojure 中的运算符与函数没法区分),并提供 100 做为部分应用的参数。Clojure 中的部分应用适用于单参数函数和多参数函数,如清单 9 中的两个示例所示。
因为 Clojure 是动态类型的,而且支持可变参数列表,所以局部套用并不能做为一种语言功能来实现。部分应用将会处理必要的状况。可是,Clojure 被添加到 reducers 库(参见 参考资料)的命名空间私有 (defcurried ...)
函数,支持在该库中更轻松地定义一些函数。鉴于 Clojure 的 Lisp 传承的灵活特色,能够轻松扩大 (defcurried ...)
的使用范围。
尽管局部套用和部分应用具备复杂的定义和大量实现细节,可是它们在实际编程中都占有一席之地。
局部套用(和部分应用)适合在传统的面向对象语言中实现工厂函数的位置使用。做为一个示例,清单 10 在 Groovy 中实现了一个简单的adder
函数。
def adder = { x, y -> x + y} def incrementer = adder.curry(1) println "increment 7: ${incrementer(7)}" // 8
在清单 10 中,我使用 adder()
函数来导出 incrementer
函数。一样,在 清单 2 中,我使用部分应用建立了一个更简洁的本地函数版本。
Gang of Four 设计模式之一是 Template Method 模式。它的用途是帮助定义算法 shell,使用内部抽象方法来实现稍后的实现灵活性。部分应用和局部套用能够解决相同的问题。使用部分应用提供已知行为,并让其余参数免费用于实现细节,这模拟了此面向对象设计模式的实现。
与 清单 2 相似,一种常见的状况是您有一系列使用类似参数值调用的函数。例如,当与持久性框架交互时,必须将数据源做为第一个参数进行传递。经过使用部分应用,能够隐式地提供值,如清单 11 所示。
(defn db-connect [data-source query params] ...) (def dbc (partial db-connect "db/some-data-source")) (dbc "select * from %1" "cust")
在清单 11 中,我使用了便利的 dbc
函数来访问数据函数,无需提供数据源,就能够自动提供数据源。面向对象编程的精髓(隐含 this
上下文彷佛出如今全部函数中)能够经过使用局部套用为全部函数提供 this
来实现,这使得它对用户不可见。
局部套用和部分应用以各类形式出如今全部 Java 下一代语言中。可使用这些技术进行更简洁的函数定义,提供隐含值,并构建函数工厂。
在下一部分中,我将介绍全部 Java 下一代语言的函数式编程功能之间存在的惊人类似之处,以及这些功能有时彻底不一样的实现细节。