學會像一個函數(shù)式程序員那樣思考
原創(chuàng)【51CTO獨家特稿】在開始進入正題之前,我們先來做一個比喻。假設你是一個伐木工人,你擁有一把這個森林里最好的斧子,而它使也你成為了當?shù)刈钣猩a(chǎn)力的伐木工人。 某一天,有人向你展示并稱贊了一個新的伐木工具--電鋸。由于銷售人員是一個非常能推銷的人,所以你買了一把電鋸回來,盡管你并不知道如何去用。于是你嘗試像以前砍樹那樣的來回擺動去鋸樹。并且你很快得出了一個結(jié)論這個新式的電鋸毫無用處,于是你又重新拿起斧子去伐木。一直到有人過來并給你演示了如何去運轉(zhuǎn)電鋸,你才明白這里的不同。
你可能聯(lián)想到了用函數(shù)式編程來代替故事中的電鋸。但是問題在于函數(shù)式編程是一種全新的編程模式,而不是一門新的語言,語法只是一個細節(jié)問題。而最不同的地方是要如何以不同的方式去思考。而我作為一名“電鋸演示者”和一個函數(shù)式程序員來到了這里。
歡迎來到函數(shù)式思維專欄。這個系列將探索函數(shù)式編程的話題,但是并不僅僅局限在函數(shù)式編程語言有關的內(nèi)容上。正如我描繪的那樣,以函數(shù)式的方法來寫代碼涉及到了設計,權衡,代碼重用和其他一系列的觀點。我會嘗試著以Java(或是類Java語言)的方式盡可能多的展示函數(shù)式編程的概念, 進而演示一些其他語言的能力-那些Java不具有的能力。當然我不會直接切入的非常深,然后討論一些時髦的事物。取而代之的是,我會逐漸演示一種新的思考問題的方式(或許你已經(jīng)在某些地方用了,但還沒有意識到)。
在接下來的兩部分里,你可以把它當作是有關于函數(shù)式編程話題的一個旅行。其中的某些概念將會有大量的細節(jié),在這個系列中我會用更多的情景和細節(jié)去描述。在旅程開始前,我將帶你看一下一個相同問題的兩個不同實現(xiàn),一個用傳統(tǒng)的方式來寫,另一個使用更多的函數(shù)式方式。
數(shù)字歸類
談論兩種不同的編程模式,你必須用代碼來做比較。第一個例子是我另一本書《The Productive Programmer》和《測試驅(qū)動設計1,2》兩篇文章中的一個變體。我選取了少量的代碼,因為在這兩篇文章里已經(jīng)深入的分析了這段代碼。這些文章對這個設計所做的稱贊并沒有錯,但我想在這里進一步提供一個不同的設計意圖。
問題的需求是這樣的:假設給定任意一個正整數(shù)都大于1,你必須按照完美的,過剩的和不足的進行歸類。一個完美數(shù)正好是它所有整除因子的總和。同樣地,一個過剩數(shù)的所有整除因子總和大于該數(shù),而一個不足數(shù)的所有整除因子總和小于該數(shù)。
快速數(shù)字歸類器
列表1中的類(NumberClassifier)滿足所有這些需求:
- public class Classifier6 {
- private Set<Integer> _factors;
- private int _number;
- public Classifier6(int number) {
- if (number < 1)
- throw new InvalidNumberException("Can't classify negative numbers");
- _number = number;
- _factors = new HashSet<Integer>>();
- _factors.add(1);
- _factors.add(_number);
- }
- private boolean isFactor(int factor) {
- return _number % factor == 0;
- }
- public Set<Integer> getFactors() {
- return _factors;
- }
- private void calculateFactors() {
- for (int i = 1; i <= sqrt(_number) + 1; i++)
- if (isFactor(i))
- addFactor(i);
- }
- private void addFactor(int factor) {
- _factors.add(factor);
- _factors.add(_number / factor);
- }
- private int sumOfFactors() {
- calculateFactors();
- int sum = 0;
- for (int i : _factors)
- sum += i;
- return sum;
- }
- public boolean isPerfect() {
- return sumOfFactors() - _number == _number;
- }
- public boolean isAbundant() {
- return sumOfFactors() - _number > _number;
- }
- public boolean isDeficient() {
- return sumOfFactors() - _number < _number;
- }
- public static boolean isPerfect(int number) {
- return new Classifier6(number).isPerfect();
- }
- }
這段代碼有幾處地方需要關注一下:
它擁有大范圍的測試(有一部分我是為了討論測試驅(qū)動開發(fā)而寫的)注:這條所說的測試位于作者另一篇文章中。
這個類由大量的緊耦合方法組成,在它的構造函數(shù)中擁有測試驅(qū)動開發(fā)的邊際效應。
在calculateFactors()方法里內(nèi)嵌了性能優(yōu)化算法。這個類的主體是由采集因子組成,因此我可以在之后對它們進行求和并進行最終的歸類。整除因子總是以成對的形式被獲取。例如,如果這個數(shù)是16,當我采集的因子為2時,我就能得到另一個因子為8,因為8x2=16。如果我獲得的因子是成對的,那么我只需要去檢查那些有平方根的數(shù),這就是calculateFactors()方法所做的事情。
更多的功能歸類
使用相同的測試開發(fā)技術,我創(chuàng)建了一個修改后的版本。列表2,更豐富的功能數(shù)字歸類器
- public class NumberClassifier {
- static public boolean isFactor(int number, int potential_factor) {
- return number % potential_factor == 0;
- }
- static public Set<Integer> factors(int number) {
- HashSet<Integer> factors = new HashSet<Integer>();
- for (int i = 1; i <= sqrt(number); i++)
- if (isFactor(number, i)) {
- factors.add(i);
- factors.add(number / i);
- }
- return factors;
- }
- static public int sum(Set<Integer> factors) {
- Iterator it = factors.iterator();
- int sum = 0;
- while (it.hasNext())
- sum += (Integer) it.next();
- return sum;
- }
- static public boolean isPerfect(int number) {
- return sum(factors(number)) - number == number;
- }
- static public boolean isAbundant(int number) {
- return sum(factors(number)) - number > number;
- }
- static public boolean isDeficient(int number) {
- return sum(factors(number)) - number < number;
- }
- }
這兩個版本的類盡管差別細微但是很重要。最主要的區(qū)別是例2的版本缺少了狀態(tài)共享。消除狀態(tài)共享在函數(shù)式編程中是比較受歡迎的一種抽象手法。作為跨方法共享狀態(tài)的替代方案,我采用直接調(diào)用的方式來消除狀態(tài)共享。從設計的角度來說,它讓factors()方法變的更長,但是它也防止了factors字段暴漏到方法之外。注意,例2是完全由靜態(tài)方法組成的。在方法間不存在知識共享的問題,因此我可以在更少函數(shù)范圍上做封裝。一旦你給它們輸入?yún)?shù)和期待值,這些方法都會工作的很好(這個是一個純函數(shù)例子,這個概念我在將來會進一步探索它)。
函數(shù)
函數(shù)式編程屬于一個寬泛的計算機科學范疇,它已經(jīng)受到了極大的關注。有新的基于JVM上開發(fā)的函數(shù)式語言(如scala和clojure)和框架(如Functional Java和Akka),它們都聲稱能夠帶來更少的缺陷,更高的生產(chǎn)力,更易讀,更賺錢等等。相比駐足門外的去解決函數(shù)式編程這一大話題,我更愿意將注意力放在一些概念以及這些概念衍生出來的話題上。
函數(shù)式編程的核心就是函數(shù), 正如在面向?qū)ο笳Z言里面類是主要的抽象那樣。函數(shù)形成了處理過程的基礎,同時它具有其他傳統(tǒng)語言沒有的一系列特性。
高階函數(shù)
高階函數(shù)可以將其他函數(shù)作為參數(shù)或者作為返回結(jié)果。這在Java語言中是無法想像的。最接近的方案是你使用一個類(通常是匿名類)作為執(zhí)行方法的“持有者”。Java沒有獨立的函數(shù)(或方法),因此它們不能作為返回值或參數(shù)出現(xiàn)。
這個能力對函數(shù)式語言來說很重要的原因有兩點:第一,擁有高階函數(shù)就意為著你可以在如何連結(jié)語言元素上作出一個假設。例如,你可以構建一個機制來消除一個類繼承體系上的一大堆方法,通過遍歷列表并對每一個元素應用一個(或多個)高階函數(shù)來實現(xiàn)。(我將展示一個簡短的例子給你)第二,通過將函數(shù)作為返回值,你有機會去創(chuàng)建一個高動態(tài),適應性的系統(tǒng)。
通過使用高階函數(shù)我們就可以使問題服從于方案,但高階函數(shù)對函數(shù)式語言來說并不是唯一的。因此,當你在使用函數(shù)式思維的時候, 你解決問題思路就會不一樣??紤]一下列表3中的例子,一個保護數(shù)據(jù)訪問的方法:
- public void addOrderFrom(ShoppingCart cart, String userName,
- Order order) throws Exception {
- setupDataInfrastructure();
- try {
- add(order, userKeyBasedOn(userName));
- addLineItemsFrom(cart, order.getOrderKey());
- completeTransaction();
- } catch (Exception condition) {
- rollbackTransaction();
- throw condition;
- } finally {
- cleanUp();
- }
- }
列表3中的代碼執(zhí)行初始化等具體任務,如果所有操作都成功就完成事務,反之回滾,并在最后清理掉資源。很明顯,代碼有一部分可以被重用,并且我們在面向?qū)ο笳Z言中也常常創(chuàng)建這樣的結(jié)構。在這個例子中,我組合使用了兩個“四人團”的設計模式:模版方法和命令模式。模版方法建議我應該移動一些通用的模版代碼到繼承體系中,并推遲算法細節(jié)到子類。命令行模式提供了一個方法以眾所周知的執(zhí)行語義來封裝行為到類,列表4就是列表3代碼應用這兩個模式之后的樣子:
列表4 重構順序后的代碼
- public void wrapInTransaction(Command c) throws Exception {
- setupDataInfrastructure();
- try {
- c.execute();
- completeTransaction();
- } catch (Exception condition) {
- rollbackTransaction();
- throw condition;
- } finally {
- cleanUp();
- }
- }
- public void addOrderFrom(final ShoppingCart cart, final String userName,
- final Order order) throws Exception {
- wrapInTransaction(new Command() {
- public void execute() {
- add(order, userKeyBasedOn(userName));
- addLineItemsFrom(cart, order.getOrderKey());
- }
- });
- }
在列表4中,我提取了一部分通用的代碼到wrapInTransaction()(這個樣式你可能認識-這是最簡單的Spring事務模版的版本)方法中,傳遞一個命令對象作為工作單元。addOrderFrom()方法包含了一個匿名內(nèi)部類的創(chuàng)建,這個類以命令模式封裝了兩個工作單元。
封裝行為純粹是Java的設計產(chǎn)物,我需要用到一個不包含任何形式的,獨立行為的命令類。Java中所有的行為都必須駐留在一個類中。甚至語言的設計者很早的就看到了這個不足,但是顯然在發(fā)布后再去考慮不將行為聯(lián)接到類上就有些晚了。因此在JDK1.1中糾正了這個缺陷,通過添加匿名內(nèi)部類的方式來實現(xiàn)。這只是以一種語法糖的方式來為少量的方法創(chuàng)建一大堆小類,這樣做僅僅是從純功能角度出發(fā),而非從結(jié)構上。如果想看有關Java這方面有趣的文章,請看Steve Yegge’s的《Execution in the Kingdom of Nouns》。
盡管我非常想要類里面的這個方法,但Java還是強制我去創(chuàng)建一個命令類的實例。這個類本身沒有任何用處:它沒有字段,沒有構造器(這個由java自動生成),并且也沒有狀態(tài)。它純粹的目的就是為了在方法里包裝行為。在函數(shù)式語言里,我們通過高階函數(shù)來取代這個模式。
如果我不準備用Java的類,那么我可能采用最接近的語義是函數(shù)式編程里面的閉包。列表5顯示了重構后的例子,但是使用Groovy代替了Java。
列表5, 使用Groovy的閉包代替命令類
- def wrapInTransaction(command) {
- setupDataInfrastructure()
- try {
- command()
- completeTransaction()
- } catch (Exception ex) {
- rollbackTransaction()
- throw ex
- } finally {
- cleanUp()
- }
- }
- def addOrderFrom(cart, userName, order) {
- wrapInTransaction {
- add order, userKeyBasedOn(userName)
- addLineItemsFrom cart, order.getOrderKey()
- }
- }
在Groovy里面,任何位于大括號{}之間的東西都是一個代碼塊,并且代碼塊可以被當作參數(shù)來模仿一個高階函數(shù)。在這種情景下,Groovy為你實現(xiàn)了命令模式。Groovy中的每一個閉包塊就是一個Groovy的閉包類型,它包含一個call()方法。當你把一對空括號放到變量后面用于保存閉包實例時,該方法會被自動調(diào)用。Groovy啟用了一些類函數(shù)式編程的行為,通過在語言本身使用相應的語法糖來構建適當?shù)臄?shù)據(jù)結(jié)構。正如我將會逐步展示的那樣,Groovy也包含其他函數(shù)式語言的能力。我將在下面的部分繼續(xù)對閉包和高階函數(shù)做一些有意思的比較。
第一級函數(shù)
函數(shù)被認為是函數(shù)式語言里面的一等公民,這就意味著函數(shù)可以出現(xiàn)在任何地方,正如其他語言的構造體(如變量)那樣。在思考不同解決方案的時候,第一級函數(shù)的存在允許函數(shù)以一種特別的方式來使用,如應用同樣的比較操作到相同的數(shù)據(jù)結(jié)構上。這就體現(xiàn)了函數(shù)式語言的一個基本思考原則:關注結(jié)果,而不是過程。
在命令式的編程語言里,我必須考慮算法的每一個原子操作。如列表1的代碼顯示的那樣。為了實現(xiàn)數(shù)字歸類器,我不得不精確的識別如何去采集整除因子,這就意為著為了確定一個因子,我不得不寫代碼去遍歷所有數(shù)字。但是像遍歷列表,然后對每一個元素實施操作,這聽起來像是很通用的東西??紤]使用Functional Java框架來重新實現(xiàn)數(shù)字歸類器的代碼,代碼如列表6所示:
列表6. 函數(shù)式的數(shù)字歸類器
- public class FNumberClassifier {
- public boolean isFactor(int number, int potential_factor) {
- return number % potential_factor == 0;
- }
- public List<Integer> factors(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(factors(number)) - number == number;
- }
- public boolean isAbundant(int number) {
- return sum(factors(number)) - number > number;
- }
- public boolean isDeficiend(int number) {
- return sum(factors(number)) - number < number;
- }
- }
列表6和列表2的不同在于兩個方法:sum()和factors()。在Functional Java里, Sum()方法具有List類的foldLeft()方法優(yōu)勢。列表操作概念上的一個具體變化就是被稱之為catamorphism,它是列表折疊上的一般化。在這里“向左折疊”的意思是:
1. 攜帶一個初始值并組合它到列表的第一個元素上
2. 攜帶結(jié)果并應用相同的操作到下一個元素上
3. 一直操作直到列表結(jié)束
注意當你對一堆數(shù)求和的時候,所做的事情是非常明顯的:從零開始,加上第一關元素,攜帶結(jié)果去加第二個,重復這個過程直到所有列表的元素都被處理。Functional Java提供高階函數(shù)(在這個例子里就是Intergers.add枚舉器)并小心翼翼的為你的代碼啟用它。(當然Java真的沒有高階函數(shù),但是你可以通過限制具體的數(shù)據(jù)結(jié)構和類型來寫一個較類似的東西)。
在列表6里面另一奇妙的方法是factors(),它充分說明了我關于“關注結(jié)果,而不是過程”的建議。發(fā)現(xiàn)一個數(shù)的整除因子這個問題的本質(zhì)是什么?換個方式來說,給出一個到目標數(shù)的所有可能數(shù)的列表。那么我該如何確定哪個數(shù)是這個數(shù)的整除因子?這里的建議是進行一次過濾操作 – 我能過濾整個列表,消除那些不符合我標準的數(shù)。這個方法基本上就如同以下描述:取得1到這個數(shù)的范圍;用f()方法來過濾這個列表,F(xiàn)unctional Java的方式將允許你創(chuàng)建一個具有特殊數(shù)據(jù)類型的類,并返回結(jié)果。
這段代碼同時也描繪了一個更大的概念,一個編程語言的趨勢?;氐竭^去,開發(fā)人員不得不處理一大堆煩人的東西,如內(nèi)存分配,垃圾回收和指針。隨著時間的推移,語言本身背負起了更多這方面的職責。就像計算機越來越強大一樣,我們把越來越多的現(xiàn)實任務丟給了語言和運行時。作為一名Java開發(fā)者,我比較傾向于把所有的內(nèi)存問題都交給語言處理。函數(shù)式編程擴大了這個需求,并包含了更多的細節(jié)。隨著時間的推移,我們將花費更少的時間去關心每一個步要解決的問題和思考的過程。隨著本系列的進展,我將展示更多相關的例子。
結(jié)論
函數(shù)式編程更多的是一種觀念而不是一個工具或者一門語言。在開始的部分,我曽提到過一些函數(shù)式編程的議題,范圍從簡單設計的討論到某些宏觀問題的重新思考。我寫了一個簡單的Java類,并讓它更符合函數(shù)式編程理念,然后開始用傳統(tǒng)的命令式語言來構建部分函數(shù)式語言的方式去深入探討了這些議題。
另外引申出兩個重點:首先是關注結(jié)果而非過程。函數(shù)式編程嘗試著使用不同的方式去表達問題,因為你已經(jīng)構建不同的代碼塊來幫助項目成長。第二點是我一直在這個系列中展示的那樣,將那些單調(diào)的細節(jié)交給編程語言和運行時去處理。這樣將有助于我們將注意力集中我們的編程問題上。在下一部分,我將繼續(xù)著眼與函數(shù)式編程語言的常規(guī)方面,并介紹如何將它應用于現(xiàn)時的軟件開發(fā)當中。