# 實戰 Groovy: 用 curry 過的閉包進行函數式編程
_Groovy 日常的編碼構造到達了閉包以前沒有到過的地方_
在 Groovy 中處處都是閉包,Groovy 閉包惟一的問題是:當每天都使用它們的時候,看起來就有點平淡了。在本文中,客座作者 Ken Barclay 和 John Savage 介紹了如何對標準的閉包(例如閉包復合和 Visitor 設計模式)進行 curry 處理。`curry()` 方法是由 Haskell Curry 發明的,在 JSR 標準發布之前就已經在 Groovy 語言中了。
幾乎從一年前_實戰 Groovy_ 系列開始,我就已經提供了多個讓您了解閉包的機會。在首次作為 _alt.lang.jre_ 系列的一部分寫 Groovy 時(“感受 Groovy”,2004 年 8 月),我介紹了 Groovy 的閉包語法,而且 [就在上一期文章中](/developerworks/cn/java/j-pg07195.html),我介紹了最新的 JSR 標準對相同語法的更新。學習至今,您知道了 Groovy 閉包是代碼塊,可以被引用、帶參數、作為方法參數傳遞、作為返回值從方法調用返回。而且,它們也可以作為其他閉包的參數或返回值。因為閉包是 `Closure` 類型的對象,所以它們也可以作為類的屬性或集合的成員。
雖然這些技術都是很神奇的,但是本期文章要學習的閉包技術要比您迄今為止嘗試過的都要更火辣一些。客座作者 John Savage 和 Ken Barclay 已經用 Groovy 閉包中的 `curry()` 方法做了一些有趣的實驗,我們非常有幸可以見識他們的技藝。
## 關于本系列
把任何一個工具集成進開發實踐的關鍵是,知道什么時候使用它而什么時候把它留在箱子中。腳本語言可以是工具箱中極為強大的附件,但是只有在恰當地應用到適當的場景中時才是這樣。為了這個目標,_實戰 Groovy_ 是一系列文章,專門介紹 Groovy 的實際應用,并教您何時以及如何成功地應用它們。
Barclay 和 Savage 的 curry 過的閉包不僅會重新激起您對熟悉的操作(例如復合)和 Visitor 設計模式的興奮,還會用 Groovy 打開函數式編程的大門。
## 進行 curry 處理
_curry 過的函數_ 一般可在函數式編程語言(例如 ML 和 Haskell)中找得到(請參閱 [參考資料](#resources))。_curry_ 這個術語來自 Haskell Curry,這個數學家發明了局部函數的概念。_Currying_ 指的是把多個參數放進一個接受許多參數的函數,形成一個新的函數接受余下的參數,并返回結果。令人興奮的消息是,Groovy 的當前版本(在編寫本文時是 jsr-02 版)支持在`閉包`上使用 `curry()` 方法 —— 這意味著,我們這些 Groovy 星球的公民,現在可以利用函數式編程的某些方面了!
您以前可能從未用過 `curry()`,所以我們先從一個簡單的、熟悉的基礎開始。清單 1 顯示了一個叫做 `multiply` 的閉包。它有形式參數 `x` 和 `y`,并返回這兩個值的乘積。假設沒有歧義存在,然后代碼演示了用于執行 `multiply` 閉包的兩種方法:顯式(通過 `call` 的方式)或隱式。后一種樣式引起了函數調用方式。
##### 清單 1\. 簡單的閉包
```
def multiply = { x, y -> return x * y } // closure
def p = multiply.call(3, 4) // explicit call
def q = multiply(4, 5) // implicit call
println "p: ${p}" // p is 12
println "q: ${q}" // q is 20
```
這個閉包當然很好,但是我們要對它進行 curry 處理。在調用 `curry()` 方法時,不需要提供所有的實際參數。_curry 過的_ 調用只引起了閉包的部分應用程序。閉包的 _部分應用程序_ 是另一個 `Closure` 對象,在這個對象中有些值已經被修正。
清單 2 演示了對 `multiply` 閉包進行 curry 處理的過程。在第一個例子中,參數 `x` 的值被設置為 3。名為 `triple` 的閉包現在有效地擁有了 `triple = { y -> return 3 * y }` 的定義。
##### 清單 2\. curry 過的閉包
```
def multiply = { x, y -> return x * y } // closure
def triple = multiply.curry(3) // triple = { y -> return 3 * y }
def quadruple = multiply.curry(4)
// quadruple = { y -> return 4 * y }
def p = triple.call(4) // explicit call
def q = quadruple(5) // implicit call
println "p: ${p}" // p is 12
println "q: ${q}" // q is 20
```
可以看到,參數 `x` 已經從 `multiply` 的定義中刪除,所有它出現的地方都被 3 這個值代替了。
### curry 過的數學 101
從基本數學可能知道,乘法運算符是_可交換的_(換句話說 `x * y = y * x`)。但是,減法運算符是不可交換的;所以,需要兩個操作來處理減數和被減數。清單 3 為這個目的定義了閉包 `lSubtract` 和 `rSubtract`(分別在左邊和右邊),結果顯示了 `curry` 函數的一個有趣的應用。
##### 清單 3\. 右和右操作數
```
def lSubtract = { x, y -> return x - y }
def rSubtract = { y, x -> return x - y }
def dec = rSubtract.curry(1)
// dec = { x -> return x - 1 }
def cent = lSubtract.curry(100)
// cent = { y -> return 100 - y }
def p = dec.call(5) // explicit call
def q = cent(25) // implicit call
println "p: ${p}" // p is 4
println "q: ${q}" // q is 75
```
* * *
## 迭代和復合
您會回憶起這個系列以前的文章中,閉包一般用于在 `List` 和 `Map` 集合上應用的_迭代器方法_ 上。例如,迭代器方法 `collect` 把閉包應用到集合中的每個元素上,并返回一個帶有新值的新集合。清單 4 演示了把 `collect` 方法應用于 `List` 和 `Map`。名為 `ages` 的 `List` 被發送給 `collect()` 方法,使用單個閉包 `{ element -> return element + 1 }` 作為參數。注意方法的最后一個參數是個閉包,在這個地方 Groovy 允許把它從實際參數列表中刪除,并把它直接放在結束括號后面。在沒有實際參數時,可以省略括號。用名為 `accounts` 的 `Map` 對象調用的 `collect()` 方法可以展示這一點。
##### 清單 4\. 閉包和集合
```
def ages = [20, 30, 40]
def accounts = ['ABC123' : 200, 'DEF456' : 300, 'GHI789' : 400]
def ages1 = ages.collect({ element -> return element + 1 })
def accounts1 = accounts.collect
{ entry -> entry.value += 10; return entry }
println "ages1: ${ages1}"
// ages1: [21, 31, 41]
println "accounts1: ${accounts1}"
// accounts1: [ABC123=210, GHI789=410, DEF456=310]
def ages2 = ages.collect { element -> return dec(element) }
println "ages2: ${ages2}"
// ages2: [19, 29, 39]
```
最后一個例子搜集名為 `ages` 的 `List` 中的所有元素,并將 `dec` 閉包 (來自清單 3)應用于這些元素。
### 閉包復合
閉包更重要的一個特征可能就是_復合(composition)_,在復合中可以定義一個閉包,它的目的是組合其他閉包。使用復合,兩個或多個簡單的閉包可以組合起來構成一個更復雜的閉包。
清單 5 介紹了一個漂亮的 `composition` 閉包。現在請注意仔細閱讀:參數 `f` 和 `g` 代表 _單個參數閉包_。到現在還不錯?現在,對于某些參數值 `x`,閉包 `g` 被應用于 x,而閉包 `f` 被應用于生成的結果!哦,只對前兩個閉包參數進行了 curry 處理,就有效地形成了一個組合了這兩個閉包的效果的新閉包。
清單 5 組合了閉包 `triple` 和 `quadruple`,形成閉包 `twelveTimes`。當把這個閉包應用于實際參數 3 時,返回值是 36。
##### 清單 5\. 超級閉包復合
```
def multiply = { x, y -> return x * y }
// closure
def triple = multiply.curry(3)
// triple = { y -> return 3 * y }
def quadruple = multiply.curry(4)
// quadruple = { y -> return 4 * y }
def composition = { f, g, x -> return f(g(x)) }
def twelveTimes = composition.curry(triple, quadruple)
def threeDozen = twelveTimes(3)
println "threeDozen: ${threeDozen}"
// threeDozen: 36
```
非常漂亮,是么?
* * *
## 五星計算
現在我們來研究閉包的一些更刺激的方面。我們先從一個機制開始,用這個機制可以表示包含_計算模式_ 的閉包,計算模式是一個來自函數式編程的概念。計算模式的一個例子就是用某種方式把 `List` 中的每個元素進行轉換。因為這些模式發生得如此頻繁,所以我們開發了一個叫做 `Functor` 的類,把它們封裝成 `static Closure`。清單 6 顯示了一個摘要。
##### 清單 6\. Functor 封裝了一個計算模式
```
package fp
abstract class Functor {
// arithmetic (binary, left commute and right commute)
public static Closure bMultiply = { x, y -> return x * y }
public static Closure rMultiply = { y, x -> return x * y }
public static Closure lMultiply = { x, y -> return x * y }
// ...
// composition
public static Closure composition = { f, g, x -> return f(g(x)) }
// lists
public static Closure map =
{ action, list -> return list.collect(action) }
public static Closure apply = { action, list -> list.each(action) }
public static Closure forAll = { predicate, list ->
for(element in list) {
if(predicate(element) == false) {
return false
}
}
return true
}
// ...
}
```
在這里可以看到名為 `map` 的閉包,不要把它與 `Map` 接口混淆。`map` 閉包有一個參數 `f` 代表閉包,還有一個參數 `list` 代表(不要驚訝)`List`。它返回一個新 `List`,其中 `f` 已經映射到 `list` 中的每個元素。當然,Groovy 已經有了用于 `Lists` 的 `collect()` 方法,所以我們在我們的實現中也使用了它。
在清單 7 中,我們把事情又向前進行了一步,對 `map` 閉包進行了 _curry_ 處理,形成一個塊,會將指定列表中的所有元素都乘以 12。
##### 清單 7\. 添加一些 curry,并乘以 12
```
import fp.*
def twelveTimes = { x -> return 12 * x }
def twelveTimesAll = Functor.map.curry(twelveTimes)
def table = twelveTimesAll([1, 2, 3, 4])
println "table: ${table}"
// table: [12, 24, 36, 48]
```
現在,_這_ 就是我們稱之為五星計算的東西!
* * *
## 業務規則用 curry 處理
關于閉包的技藝的討論很不錯,但是更關注業務的人會更欣賞下面這個例子。在考慮計算特定 `Book` 條目凈值的問題時,請考慮商店的折扣和政府的稅金(例如 _增值稅_)。如果想把這個邏輯作為 `Book` 類的一部分包含進來,那么形成的解決方案可能是個難纏的方案。因為書店可能會改變折扣,或者折扣只適用于選定的存貨,所以這樣一個解決方案可能會太剛性了。
但是猜猜情況如何。變化的業務規則非常適合于使用 curry 過的閉包。可以用一組簡單的閉包來表示單獨的業務規則,然后用復合把它們以不同的方式組合起來。最后,可以用 _計算模式_ 把它們映射到集合。
清單 8 演示了書店的例子。閉包 `rMultiply` 是個局部應用程序,通過使用一個不變的第二操作數,把二元乘法改變成一元閉包。兩個圖書閉包 `calcDiscountedPrice` 和 `calcTax` 是 `rMultiply` 閉包的實例,為乘數值設置了值。閉包 `calcNetPrice` 是計算凈價的算法:先計算折扣價格,然后在折扣價格上計算銷售稅。最后, `calcNetPrice` 被應用于圖書價格。
##### 清單 8\. 圖書業務對象
```
import fp.*
class Book {
@Property name
@Property author
@Property price
@Property category
}
def bk = new Book(name : 'Groovy', author :
'KenB', price : 25, category : 'CompSci')
// constants
def discountRate = 0.1
def taxRate = 0.17
// book closures
def calcDiscountedPrice = Functor.rMultiply.curry(1 - discountRate)
def calcTax = Functor.rMultiply.curry(1 + taxRate)
def calcNetPrice =
Functor.composition.curry(calcTax, calcDiscountedPrice)
// now calculate net prices
def netPrice = calcNetPrice(bk.price)
println "netPrice: ${netPrice}" // netPrice: 26.325
```
* * *
## 更加 groovy 的訪問者
已經看到了如何把 curry 過的閉包應用于函數模式,所以現在我們來看看在使用相似的技術重新訪問重要的面向對象設計模式時發生了什么。對于面向對象系統來說,必須遍歷對象集合并在集合中的每個元素上執行某個操作,是非常普通的使用情況。假設一個不同的場景,系統要遍歷相同的集合,但是要執行不同的操作。通常,需要用 _Visitor 設計模式_(請參閱 [參考資料](#resources))來滿足這一需求。`Visitor` 接口引入了處理集合元素的動作協議。具體的子類定義需要的不同行為。方法被引進來遍歷集合并對每個元素應用 `Visitor` 動作。
如果到現在您還沒猜出來,那么可以用閉包實現同的效果。這種方法的一個抽象是:使用閉包,不需要開發訪問者類的層次結構。而且,可以有效地使用閉包復合和映射來定義集合的動作和效果遍歷。
例如,考慮用來表示圖書館庫存的類 `Library` 和類 `Book` 之間的一對多關系。可以用 `List` 或 `Map` 實現這個關系;但是 `Map` 提供的優勢是它能提供快速的查詢,也就是說用圖書目錄編號作為鍵。
清單 9 顯示了一個使用 `Map` 的簡單的一對多關系。請注意 `Library` 類中的兩個顯示方法。引入訪問者時,兩者都是重構的目標。
##### 清單 9\. 圖書館應用程序
```
class Book {
@Property title
@Property author
@Property catalogNumber
@Property onLoan = false
String toString() {
return "Title: ${title}; author: ${author}"
}
}
class Library {
@Property name
@Property stock = [ : ]
def addBook(title, author, catalogNumber) {
def bk = new Book(title : title, author :
author, catalogNumber : catalogNumber)
stock[catalogNumber] = bk
}
def lendBook(catalogNumber) {
stock[catalogNumber].onLoan = true
}
def displayBooksOnLoan() {
println "Library: ${name}"
println "Books on loan"
stock.each { entry ->
if(entry.value.onLoan == true) println entry.value
}
}
def displayBooksAvailableForLoan() {
println "Library: ${name}"
println "Books available for loan"
stock.each { entry ->
if(entry.value.onLoan == false) println entry.value
}
}
}
def lib = new Library(name : 'Napier')
lib.addBook('Groovy', 'KenB',
'CS123')
lib.addBook('Java', 'JohnS', 'CS456')
lib.addBook('UML', 'Ken and John',
'CS789')
lib.lendBook('CS123')
lib.displayBooksOnLoan() // Title: Groovy; author: KenB
lib.displayBooksAvailableForLoan() // Title: UML; author: Ken and John
// Title: Java; author: JohnS
```
清單 10 包含 `Library` 類中的幾個閉包,模擬了訪問者的用法。`action` 閉包(與 `map` 閉包有點相似)把 `action` 閉包應用于 `List` 的每個元素。如果某本書被借出,則閉包 `displayLoanedBook` 顯示它;如果某本書未被借出,則閉包 `displayAvailableBook` 顯示它。兩者都扮演訪問者和相關的動作。用 `displayLoanedBook` 對 `apply` 閉包進行 curry 處理,會形成閉包 `displayLoanedBooks`,它為處理圖書集合做好了準備。類似的方案也用來生成可供借閱的圖書顯示,如清單 10 所示。
##### 清單 10\. 修訂后的圖書館訪問者
```
import fp.*
class Book {
@Property title
@Property author
@Property catalogNumber
@Property onLoan = false
String toString() {
return " Title: ${title}; author: ${author}"
}
}
class Library {
@Property name
@Property stock = [ : ]
def addBook(title, author, catalogNumber) {
def bk = new Book(title : title, author :
author, catalogNumber : catalogNumber)
stock[catalogNumber] = bk
}
def lendBook(catalogNumber) {
stock[catalogNumber].onLoan = true
}
def displayBooksOnLoan() {
println "Library: ${name}"
println "Books on loan"
displayLoanedBooks(stock.values())
}
def displayBooksAvailableForLoan() {
println "Library: ${name}"
println "Books available for loan"
displayAvailableBooks(stock.values())
}
private displayLoanedBook = { bk -> if(bk.onLoan == true)
println bk }
private displayAvailableBook = { bk -> if(bk.onLoan == false)
println bk }
private displayLoanedBooks =
Functor.apply.curry(displayLoanedBook)
private displayAvailableBooks =
Functor.apply.curry(displayAvailableBook)
}
def lib = new Library(name : 'Napier')
lib.addBook('Groovy', 'KenB',
'CS123')
lib.addBook('Java', 'JohnS', 'CS456')
lib.addBook('UML', 'Ken and John',
'CS789')
lib.lendBook('CS123')
lib.displayBooksOnLoan() // Title: Groovy; author: KenB
lib.displayBooksAvailableForLoan() // Title: UML; author: Ken and John
// Title: Java; author: JohnS
```
* * *
## 用閉包進行測試
在結束之前,我們來看一下 Groovy 閉包的一個附加用途。請考慮一個被建模為具有許多 `Employee` 的 `Company` 的應用程序。遞歸的關系在單個 `Employee`(團隊領導)和許多 `Employee`(團隊成員)之間進一步建立起一對多的聚合。圖 1 是這樣一個組織的類圖。
##### 圖 1\. Company 應用程序

可以用閉包來表述模型架構上的完整性。例如,在這個例子中,可能想確保每個員工都分配了一個經理。簡單的閉包 `hasManager` 為每個員工表達了這個需求: `def hasManager = { employee -> return (employee.manager != null) }`。
來自清單 6 的 `Functor` 類中的 `forAll` 閉包的局部應用程序能夠描述架構的需求:`def everyEmployeeHasManager = Functor.forAll.curry(hasManager)`。
清單 11 演示了 curry 過的閉包的應用:測試系統架構的完整性。
##### 清單 11\. 用于測試架構完整性的閉包
```
import fp.*
/**
* A company with any number of employees.
* Each employee is responsible
* to a team leader who, in turn, manages a team of staff.
*/
import java.util.*
class Employee {
@Property id
@Property name
@Property staff = [ : ]
@Property manager = null
String toString() {
return "Employee: ${id} ${name}"
}
def addToTeam(employee) {
staff[employee.id] = employee
employee.manager = this
}
}
class Company {
@Property name
@Property employees = [ : ]
def hireEmployee(employee) {
employees[employee.id] = employee
}
def displayStaff() {
println "Company: ${name}"
println "===================="
employees.each { entry -> println "
${entry.value}" }
}
}
def co = new Company(name : 'Napier')
def emp1 = new Employee(id : 123, name : 'KenB')
def emp2 = new Employee(id : 456, name : 'JohnS')
def emp3 = new Employee(id : 789, name : 'JonK')
co.hireEmployee(emp1)
co.hireEmployee(emp2)
co.hireEmployee(emp3)
emp3.addToTeam(emp1)
emp3.addToTeam(emp2)
co.displayStaff()
// Architectural closures
def hasManager = { employee -> return (employee.manager != null) }
def everyEmployeeHasManager = Functor.forAll.curry(hasManager)
def staff = new ArrayList(co.employees.values())
println "Every employee has a manager?:
${everyEmployeeHasManager.call(staff)}" // false
```
* * *
## curry 是優秀的
在本文中您已看到了大量閉包,但是希望能激起您對更多閉包的渴望。在學習乘法例子時,curry 過的閉包使得實現計算的函數模式出奇得容易。一旦掌握了這些模式,就可以把它們部署到常見的企業場景中,例如我們在書店例子中把它們應用于業務規則。把閉包應用于函數模式是令人興奮的,一旦這么做了之后,再把它們應用于面向對象設計模式,就不是什么大事情了。Curry 過的閉包可以用來模擬 Visitor 模式的基本元素,正如在 `Library` 例子中顯示的。它們在軟件測試期間執行完整性測試時也會有用,就像用 `Company` 例子所展示的。
本文中看到的全部例子都是企業系統常見的用例。看到 Groovy 閉包和 `curry` 方法能夠如此流暢地應用于眾多編程場景、函數模式和面向對象模式,真是激動人心。Haskell Curry 肯定發現了這個可怕的 _groovy_!
- 實戰 Groovy
- 實戰 Groovy: SwingBuilder 和 Twitter API,第 2 部分
- 實戰 Groovy: SwingBuilder 和 Twitter API,第 1 部分
- 實戰 Groovy: @Delegate 注釋
- 實戰 Groovy: 使用閉包、ExpandoMetaClass 和類別進行元編程
- 實戰 Groovy: 構建和解析 XML
- 實戰 Groovy: for each 剖析
- 實戰 Groovy: Groovy:Java 程序員的 DSL
- 實戰 Groovy: 關于 MOP 和迷你語言
- 實戰 Groovy: 用 curry 過的閉包進行函數式編程
- 實戰 Groovy: Groovy 的騰飛
- 實戰 Groovy: 在 Java 應用程序中加一些 Groovy 進來
- 實戰 Groovy: 用 Groovy 生成器作標記
- 實戰 Groovy: 用 Groovy 打造服務器端
- 實戰 Groovy: 使用 Groovy 模板進行 MVC 編程
- 實戰 Groovy: 用 Groovy 進行 JDBC 編程
- 實戰 Groovy: 用 Groovy 進行 Ant 腳本編程
- 實戰 Groovy: 用 Groovy 更迅速地對 Java 代碼進行單元測試
- alt.lang.jre: 感受 Groovy