|
在本系列的 第一期 中,我首先討論函數(shù)編程的一些特點(diǎn),并演示如何在 Java 和其他函數(shù)語(yǔ)言中體現(xiàn)這些觀念。在本文中,我將繼續(xù)討論這些概念,講解一級(jí)函數(shù)、優(yōu)化和閉包。但本期的內(nèi)在主題是控制:什么時(shí)候想要控制、什么時(shí)候需要控制、什么時(shí)候應(yīng)該放棄控制。
一級(jí)(First-class)函數(shù)和控制
我在上一部分最后通過使用 Functional Java 庫(kù)(見 參考資料),演示了用函數(shù) isFactor() 和 factorsOf() 方法實(shí)現(xiàn)數(shù)字分類器,如清單 1 所示:
清單 1. 數(shù)字分類器的函數(shù)版本
public class FNumberClassifier {
public boolean isFactor(int number, int potential_factor) {
return number % potential_factor == 0;
}
public List<Integer> factorsOf(final int number) {
return range(1, number+1).filter(new F<Integer, Boolean>() {
public Boolean f(final Integer i) {
return number % i == 0;
}
});
}
public int sum(List<Integer> factors) {
return factors.foldLeft(fj.function.Integers.add, 0);
}
public boolean isPerfect(int number) {
return sum(factorsOf(number)) - number == number;
}
public boolean isAbundant(int number) {
return sum(factorsOf(number)) - number > number;
}
public boolean isDeficiend(int number) {
return sum(factorsOf(number)) - number < number;
}
}
|
在 isFactor() 和 factorsOf() 方法中,我停止了對(duì)框架循環(huán)算法的控制 —
它決定如何通過最好的方式遍歷所有數(shù)字。如果框架(或者 — 如果您選擇一門函數(shù)語(yǔ)言,如 Clojure 或 Scala —
語(yǔ)言)能優(yōu)化底層實(shí)現(xiàn),那么您就可以自動(dòng)從中獲益。盡管您一開始可能不愿放棄這么多控制,但要知道這是編程語(yǔ)言和運(yùn)行時(shí)的普遍趨勢(shì):隨著時(shí)代發(fā)展,開發(fā)人
員會(huì)越來越遠(yuǎn)離那些平臺(tái)能更有效處理的細(xì)節(jié)。我從不擔(dān)心 JVM
的內(nèi)存管理,因?yàn)槠脚_(tái)可以讓我忘了它。當(dāng)然,有時(shí)候它也會(huì)讓事情變得更復(fù)雜,但是對(duì)于您從日復(fù)一日的編碼中獲得的收益來說,這是值得的。函數(shù)語(yǔ)言結(jié)構(gòu),如
高階和一級(jí)函數(shù),能讓我對(duì)抽象的理解更進(jìn)一步,讓我更多地將精力放在代碼能做什么 而不是怎么做 上。
即使使用 Functional Java 框架,在 Java 中以這種風(fēng)格編程也很麻煩,因?yàn)檫@種語(yǔ)言并沒有真正的這類語(yǔ)法和結(jié)構(gòu)。在支持的語(yǔ)言中進(jìn)行函數(shù)編碼是什么樣的呢?
Clojure 中的分類器
Clojure 是一種用于 JVM 的 Lisp(見 參考資料)??纯从?Clojure 編寫的數(shù)字分類器,如清單 2 所示:
清單 2. 數(shù)字分類器的 Clojure 實(shí)現(xiàn)
(ns nealford.perfectnumbers)
(use '[clojure.contrib.import-static :only (import-static)])
(import-static java.lang.Math sqrt)
(defn is-factor?[factor number]
(= 0 (rem number factor)))
(defn factors [number]
(set (for [n (range 1 (inc number)) :when (is-factor? n number)] n)))
(defn sum-factors [number]
(reduce + (factors number)))
(defn perfect?[number]
(= number (- (sum-factors number) number)))
(defn abundant?[number]
(< number (- (sum-factors number) number)))
(defn deficient?[number]
(> number (- (sum-factors number) number)))
|
即使您不是熟練的 Lisp 開發(fā)人員,也能輕松讀懂 清單 2 中的大多數(shù)代碼 — 您可以學(xué)著從內(nèi)向外讀。例如,is-factor? 方法有兩個(gè)參數(shù),它判斷 number 除以 factor 時(shí)余數(shù)是否等于零。同樣,perfect?、abundant? 和 deficient? 方法也很容易理解,尤其是參考一下 清單 1 的 Java 實(shí)現(xiàn)之后。
sum-factors 方法使用內(nèi)置的 reduce 方法。sum-factors 一次減少一個(gè)列表元素,它使用函數(shù)(本例中,是 +)作為每個(gè)元素的第一個(gè)參數(shù)。reduce 方法在幾種語(yǔ)言和框架中會(huì)有不同的形式;清單 1 是 foldLeft() 方法的 Functional Java 版本。factors 方法會(huì)返回一個(gè)數(shù)字列表,因此我一次處理一個(gè)數(shù)字,將每個(gè)元素加入和中,該和數(shù)就是 reduce 的返回值。您會(huì)看到,一旦您習(xí)慣了從高階和一級(jí)函數(shù)的角度思考,您就能減少(一語(yǔ)雙關(guān))代碼中無(wú)用的部分。
factors 方法可能看上去像一個(gè)隨機(jī)符號(hào)集。但如果您理解了列表的含義,您就知道這么做是有道理的,這是 Clojure 中強(qiáng)大的列表操作功能之一。和之前一樣,從內(nèi)向外閱讀 factors 就很容易理解。不要被這些混在一起的語(yǔ)言術(shù)語(yǔ)搞糊涂。Clojure 中的 for 關(guān)鍵詞并不表示 for 循環(huán)。相反,將它當(dāng)成是所有過濾和轉(zhuǎn)換結(jié)構(gòu)的來源。本例中,我讓它過濾從 1 到(number + 1)范圍的數(shù)字,使用 is-factor? 謂詞(我在之前的 清單 2 中定義的 is-factor 方法 — 請(qǐng)注意一類函數(shù)的大量使用),返回匹配的數(shù)字。從此操作返回的是一組滿足過濾標(biāo)準(zhǔn)的數(shù)字列表,我將其放入一個(gè)集合來刪除重復(fù)值。
盡管學(xué)習(xí)一門新的語(yǔ)言很麻煩,但對(duì)于函數(shù)語(yǔ)言,當(dāng)您了解其特點(diǎn)后,學(xué)起來就容易得多。
優(yōu)化
轉(zhuǎn)換到函數(shù)樣式的收益之一就是能利用語(yǔ)言或框架提供的高階函數(shù)。那么不想放棄控制的時(shí)候呢?我在之前的例子中,把遍歷機(jī)制的內(nèi)部行為比作內(nèi)存管理器的內(nèi)部運(yùn)作:大多數(shù)時(shí)候,您會(huì)很高興不用關(guān)心那些細(xì)節(jié)。但有時(shí)候您會(huì)關(guān)心這些問題,特別是在遇到優(yōu)化或類似任務(wù)時(shí)。
在 “運(yùn)用函數(shù)式思維,第 1 部分” 的數(shù)字分類器的兩個(gè) Java 版本中,我優(yōu)化了確定因子的代碼。原先是簡(jiǎn)單的使用取模運(yùn)算符(%)的實(shí)現(xiàn),它非常低效,它自己檢查從 2 到目標(biāo)數(shù)的每個(gè)數(shù)字,確定是否是因子。因子是成對(duì)出現(xiàn)的,可以通過這點(diǎn)來優(yōu)化算法。例如,如果您查找 28 的因子,當(dāng)您找到 2 時(shí),那么同時(shí)會(huì)找到 14。如果您成對(duì)獲取因子,您只需要檢查到目標(biāo)數(shù)的平方根即可。
在 Java 版本中很容易完成的實(shí)現(xiàn)似乎在 Functional Java 版本中很難做到,因?yàn)槲覠o(wú)法直接控制遍歷機(jī)制。但作為函數(shù)式思維的一部分,您需要放棄這種控制觀念,學(xué)會(huì)用另一種控制。
我會(huì)以函數(shù)式思維重新說明原來的問題:過濾所有 1 到 number 的因子,只保留匹配 isFactor() 謂詞的因子。其實(shí)現(xiàn)見清單 3:
清單 3. isFactor() 方法
public List<Integer> factorsOf(final int number) {
return range(1, number+1).filter(new F<Integer, Boolean>() {
public Boolean f(final Integer i) {
return number % i == 0;
}
});
}
|
盡管看上去很優(yōu)雅,但 清單 3 中的代碼效率很低,因?yàn)樗鼤?huì)檢查每個(gè)數(shù)。在了解優(yōu)化(成對(duì)獲取因子,只檢查到平方根)之后,重述問題如下:
- 過濾目標(biāo)數(shù)的所有因子,從 1 到其平方根。
- 用這些因子除以目標(biāo)數(shù),以獲得對(duì)稱因子,并將它加入因子列表中。
記住了這個(gè)目標(biāo),我就可以用 Functional Java 庫(kù)寫出 factorsOf() 方法的優(yōu)化版本,如清單 4 所示:
清單 4. 優(yōu)化的因子查找方法
public List<Integer> factorsOfOptimzied(final int number) {
List<Integer> factors =
range(1, (int) round(sqrt(number)+1))
.filter(new F<Integer, Boolean>() {
public Boolean f(final Integer i) {
return number % i == 0;
}});
return factors.append(factors.map(new F<Integer, Integer>() {
public Integer f(final Integer i) {
return number / i;
}}))
.nub();
}
|
清單 4
中的代碼是基于我之前講過的算法,其中有一些獨(dú)特的語(yǔ)法,這是 Functional Java 框架所必需的。首先,獲取數(shù)的范圍是從 1
到目標(biāo)數(shù)的平方根加 1(確保能取到所有因子)。第二步,根據(jù)與之前版本一樣的取模操作方法過濾結(jié)果,這些都包含在 Functional Java
代碼段中。我將過濾后的列表放在 factors 變量中。第四步(從內(nèi)到外閱讀),獲取因子列表,并執(zhí)行 map() 函數(shù),它在代碼中對(duì)每個(gè)元素進(jìn)行處理(將每個(gè)元素 映射到一個(gè)新值),從而產(chǎn)生一個(gè)新的列表。因子列表中包含到目標(biāo)數(shù)平方根的所有因子;需要除以每個(gè)數(shù)以獲得對(duì)稱因子,而 map() 方法就是完成這個(gè)任務(wù)的。第五步,現(xiàn)在已經(jīng)有了對(duì)稱因子列表,我將它添加到原來的列表中。最后一步,有個(gè)情況我必需考慮,因子保存在 List 中,而不是 Set 中。List 方法對(duì)于這些類型操作很方便,但我的算法有個(gè)副作用,就是出現(xiàn)整數(shù)平方根時(shí),會(huì)有重復(fù)。例如,如果目標(biāo)數(shù)是 16,平方根的整部部分 4 會(huì)在因子列表中出現(xiàn)兩次。為了能繼續(xù)使用方便的 List 方法,我只要在最后調(diào)用 nub() 方法,它將刪除所有重復(fù)值。
一般情況下在使用高級(jí)抽象,比如函數(shù)編程時(shí),您不需要了解實(shí)現(xiàn)細(xì)節(jié),但這并不意味這在必要的情況下,就無(wú)法了解。Java
平臺(tái)大多數(shù)情況下不需要您知道底層內(nèi)容,但如果您下定決心,您就可以了解到你想達(dá)到的層次的內(nèi)容。同樣,在函數(shù)編程結(jié)構(gòu)中,您可以把細(xì)節(jié)留給抽象機(jī)制,在
出現(xiàn)問題的時(shí)候才去關(guān)注它。
到目前為止所演示的 Functional Java 代碼中,最精華的部分是代碼的語(yǔ)法,它使用了泛型和匿名內(nèi)部類作為偽代碼段和閉包類型結(jié)構(gòu)。閉包是函數(shù)語(yǔ)言的共有特征之一。為什么它們用處這么大?
回頁(yè)首 閉包有什么特別之處?
閉包是一個(gè)會(huì)對(duì)它內(nèi)部引用的所有變量進(jìn)行隱式綁定的函數(shù)。換句話說,這個(gè)函數(shù)(或方法)會(huì)對(duì)它引用的所有內(nèi)容關(guān)閉上下文。閉包經(jīng)常會(huì)在函數(shù)語(yǔ)言和框架中用作可移動(dòng)執(zhí)行機(jī)制,會(huì)作為轉(zhuǎn)換代碼傳遞給高階函數(shù),如 map()。Functional Java 使用匿名內(nèi)部閉包來模仿 “實(shí)際” 閉包行為,但不能一直這樣做,因?yàn)?Java 不支持閉包。這意味著什么?
清單 5 是一個(gè)樣例,演示了閉包的特別之處。這是用 Groovy 編寫的,它通過代碼段塊機(jī)制支持閉包。
清單 5. Groovy 代碼演示閉包
def makeCounter() {
def very_local_variable = 0
return { return very_local_variable += 1 }
}
c1 = makeCounter()
c1()
c1()
c1()
c2 = makeCounter()
println "C1 = ${c1()}, C2 = ${c2()}"
// output:C1 = 4, C2 = 1
|
makeCounter() 方法首先定義了一個(gè)具有適當(dāng)名稱的本地變量,然后返回使用該變量的代碼段。請(qǐng)注意,makeCounter() 方法的返回類型是代碼段,而不是值。代碼段做的事就是增加本地變量的值并返回。我在代碼中設(shè)置了顯式 return 調(diào)用,這在 Groovy 中都是可選的,但如果不用,代碼就更難懂了!
為了能使用 makeCounter() 方法,我將代碼段指定為 C1 變量,然后調(diào)用三次。我使用了 Groovy 的語(yǔ)法糖(syntactic sugar)來執(zhí)行此代碼段,結(jié)果放置在代碼段變量旁邊的圓括號(hào)中。接下來,我再次調(diào)用 makeCounter(),將代碼段的一個(gè)新實(shí)例指定為 C2。最后,我再次執(zhí)行 C1 和 C2。請(qǐng)注意,每個(gè)代碼段都能追蹤到 very_local_variable 的一個(gè)單獨(dú)實(shí)例。這就是封閉的上下文 的含義。即使在方法內(nèi)部定義本地變量,代碼段仍會(huì)綁定到該變量,因?yàn)樽兞恳昧舜a段,這意味著在代碼段可用時(shí),變量必須能追蹤到它。
在 Java 中實(shí)現(xiàn)相同操作的最簡(jiǎn)單的方法如清單 6:
清單 6. Java 中的 Counter
public class Counter {
private int varField;
public Counter(int var) {
varField = var;
}
public static Counter makeCounter() {
return new Counter(0);
}
public int execute() {
return ++varField;
}
}
|
對(duì) Counter 類進(jìn)行一些變化都可以,但您一定要自己管理狀態(tài)。以上演示了為什么閉包體現(xiàn)了函數(shù)思想:讓運(yùn)行時(shí)管理狀態(tài)。與其強(qiáng)迫自己處理字段創(chuàng)建和原始狀態(tài)(包括在多線程環(huán)境中使用代碼的可怕前景),還不如讓語(yǔ)言或框架默默地管理狀態(tài)。
在以后的 Java 版本中會(huì)有閉包(關(guān)于本話題的討論已超出本文范圍)。Java
中包含它們將有兩項(xiàng)收益。首先,在改進(jìn)語(yǔ)法的同時(shí),大大簡(jiǎn)化框架和庫(kù)作者的能力。第二,為所有在 JVM 上運(yùn)行的語(yǔ)言提供一個(gè)底層的共同基礎(chǔ)。即使很多
JVM 語(yǔ)言支持閉包,但它們都實(shí)現(xiàn)自己的版本,這造成在語(yǔ)言之間傳遞閉包非常麻煩。如果 Java
定義了一個(gè)單一格式,那么其他所有語(yǔ)言都能利用它。
回頁(yè)首 結(jié)束語(yǔ)
回避對(duì)底層細(xì)節(jié)的控制是軟件開發(fā)的普遍趨勢(shì)。我們很高興不用再操心垃圾回收、內(nèi)存管理和硬件差異。函數(shù)編程代表了下一個(gè)抽象階段:將更加瑣碎的細(xì)節(jié)如遍
歷、并發(fā)性和狀態(tài)盡可能留給運(yùn)行時(shí)處理。這并不意味著您在需要的時(shí)候無(wú)法再控制它們 — 是您想要控制,而不是被迫控制。
在下一篇文章中,我將繼續(xù)探討 Java 及同類語(yǔ)言中的函數(shù)編程結(jié)構(gòu),我將會(huì)介紹 currying 和 partial method application。
參考資料 學(xué)習(xí) 獲得產(chǎn)品和技術(shù) 討論
關(guān)于作者 Neal Ford 是一家全球性 IT 咨詢公司 ThoughtWorks 的軟件架構(gòu)師和 Meme Wrangler。他的工作還包括設(shè)計(jì)和開發(fā)應(yīng)用程序、教材、雜志文章、課件和視頻/DVD 演示,而且他是各種技術(shù)書籍的作者或編輯,包括最近的新書
The Productive Programmer
。他主要的工作重心是設(shè)計(jì)和構(gòu)建大型企業(yè)應(yīng)用程序。他還是全球開發(fā)人員會(huì)議上的國(guó)際知名演說家。請(qǐng)?jiān)L問他的 Web 站點(diǎn)。
|