局部套用 和部分應用 是來源於數學的語言技術(基於 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 下一代語言的函數式編程功能之間存在的驚人類似之處,以及這些功能有時徹底不一樣的實現細節。