Scala如何改變了我的編程風格:從命令式到函數式
原創51CTO編輯推薦:Scala編程語言專題
【51CTO快譯】編者前言:這篇文章最初寫于2008年底,作者Bill Venners一方面是美國著名開發網站Artima的總編,另一方面也是一位十分關注Scala語言的Java程序員。在這幾個月間的Scala創始人Martin Odersky訪談系列中,與Martin對話的正是Bill Venners。這篇文章雖然已經完成了半年有余,但對于還不很熟悉Scala語言的Java程序員而言,仍然是一篇非常實用的Scala語言簡介。以下是譯文:
每次我學習一門新的語言,我都會學到某些編程方面的東西。比如說,當我以一個C++程序員的身份學習Java的時候,Java的接口構造教會我來自純粹的抽象基類的多重繼承的價值。盡管在C++里面這種編程風格是有可能的,但在我使用C++的日子里,我卻沒有考慮用這種方式進行多重繼承,而我在C++設計中也不怎么使用抽象基類。然而,一旦我開始進行Java編程,我就開始一直使用這種風格了。學習Java—尤其是它的接口構造—改變了我OO設計的方法。
51CTO編輯推薦:充分利用面向對象語言的接口特性 | 面向對象的思維過程
我學習Scala編程的時候也發生了類似的情況。在過去的兩年里,我有相當多的時間是用Scala工作的,Scala是Java平臺上的一種新的靜態類型語言,它融合了面向對象編程和函數型程序設計的概念。Scala能讓我寫出幾乎跟Ruby和Python一樣簡潔的代碼。在Scala我可以跟在Java里面一樣方便地調用Java庫,包括我已有的Java庫。考慮到Scala是靜態類型的,我可以享受到諸多靜態類型的好處,諸如將文檔作為類型,IDE代碼自動完成,動態代碼重構(deterministic refactoring)以及執行速度等(Scala程序的執行速度跟Java的一樣快)。但Scala還讓我以簡潔和類型安全的方式獲得某些通常是動態語言的好處,例如在已有類上增加新方法的能力,或者將類型傳遞給沒有共同繼承關系的方法。
Scala是怎樣改變了我對編程的看法的呢?一句話:我學會了欣賞函數化的風格。函數化的編程風格強調不可變對象、變量可被初始化但不能重新賦值(Java中的最終變量)、數據結構轉換,以及方法和控制的構造,最終產生一個沒有副作用的結果。這個領域的另一端是命令式的風格,以可變對象、變量可被重新賦值(Java里的正常變量)、在數據結構中索引、以及帶副作用的方法和控制構造為特征。
盡管Scala經常被吹捧為函數型編程語言,當它并不僅僅是函數型的。Scala同時支持函數式和命令式兩種風格。如果你自己選擇要這么做的話,你可以以Java的編程方式進行Scala編程,那種風格主要是命令式的。這樣有助于Scala的學習曲線變緩,但隨著對Scala越來越熟悉,你就會發現自己會更喜歡函數式的。我就是這樣。為什么?因為我發現函數型風格往往要比命令式風格的代碼更簡潔,且更不易出錯。函數式風格的代碼通常層次更高,這使得它編寫起來更快,閱讀也更為容易。舉個例子,看看下面這段確定一個字符串是否包含大寫字符的Java代碼。
for (int i = 0; i < name.length(); ++i) { if (Character.isUpperCase(name.charAt(i))) { nameHasUpperCase = true; break; } }
boolean nameHasUpperCase = false; // 這是Java
這里的命令式風格是很明顯的,因為nameHasUpperCase變量被重新賦值會給loop循環帶來副作用,loop是通過字符串中的字母索引進行迭代的。在Java你還可以以更為簡潔的方式得到相同的結果,像下面這樣:
boolean nameHasUpperCase = !name.toLowerCase().equals(name);
這一行Java代碼展現出一種更為函數化的風格,因為它轉換不可變數據:name這個字符串被轉換為另外一個全部字母都是小寫的字符串,然后值被轉換為布爾結果。此外,nameHasUpperCase這個變量被初始化了,但僅限于這一小塊代碼里,而沒有被重新賦值。如果該變量為最終值的話,它的函數化就會更為清晰。
在Scala里面,你可以寫出跟以上兩個例子類似的代碼,不過更為理想的編寫方式是像下面這樣的:
val nameHasUpperCase = name.exists(_.isUpperCase)
nameHasUpperCase變量被定義為 val,即可被初始化但不能被重新賦值的變量(類似于Java里面的最終變量)。甚至于盡管本例中并無顯式的類型標注,Scala的類型推斷機制也會給nameHasUpperCase賦予Boolean類型。exists 方法在對象集合中迭代,并依次將每個元素傳遞給函數對象。在這里, name字符串被視為字符集合,因此exists會把字符串的每一個字符都傳遞給該函數。_.isUpperCase的語法是Scala里的一種函數顯式聲明(function literal),是一種編寫少量代碼就可以到處傳遞和調用的速寫方式。下劃線代表該函數的唯一參數。因此你可以把下劃線視為每次該函數被調用時待填的空白。如果exists 方法發現該函數因被傳遞的字符中的其中一個而返回true—比如說,其中一個字符是大寫的—而返回true。否則就返回false。
盡管最后的這個單行代碼對于某些不熟悉Scala的人來說像是天書,只要你了解了Scala,你就能一眼看出代碼的目的。相反,其他的兩個版本卻要費上一點功夫去研究一下。另外需要注意的一點不同是命令式例子中潛在的偏移錯誤,因為你必須顯式地指出迭代的上標。在函數化的版本里這種錯誤不會產生,在這種方式下,函數化版本相對而言不易出錯。
最后,我想指出的是我轉向Scala的時候并沒有“徹底函數化”。盡管我已經發現通常大部分情況下函數化風格的代碼來得更為簡潔、明晰,更不易出錯,我還發現有時候命令式風格也可帶來更為清晰和簡潔的代碼。在那種情況下,我就會使用命令式的。Scala允許我方便地應用函數式和命令式的風格,結合使用此二者,我就能找到寫出清晰代碼的最佳方式。
#p#
函數式編程和命令式編程簡介
什么是函數式編程?(參考資料:《征服RIA:基于JavaScript的Web客戶端開發》第8章JavaScript函數對象)
在數學領域,函數是一種關系,這種關系使一個集合里的每一個元素對應到另一個集合里的唯一元素。函數是將唯一的輸出值賦予每一輸入的"法則"。這一"法則"可以用函數表達式、數學關系,或者一個將輸入值與輸出值對應列出的簡單表格來表示。函數最重要的性質是其決定性,即同一輸入總是對應同一輸出(注意,反之未必成立)。從這種視角,可以將函數看做"機器"或者"黑盒",它將有效的輸入值變換為唯一的輸出值。通常將輸入值稱做函數的參數,將輸出值稱做函數的值。
《Why Functional Programming Matters》的作者John Hughes 說明了模塊化是成功編程的關鍵,而函數編程可以極大地改進模塊化。在函數編程中,編程人員有一個天然框架用來開發更精練的、更小的、更簡單的和更一般化的模塊,然后將它們組合在一起。函數式編程的基本特點是:
豐富的數據類型;
函數是運算元;
在函數內保存數據;
函數內的運算對函數外無副作用。
函數式編程只描述在程序輸入上執行的操作,不必使用臨時變量保存中間結果。重點是捕捉"是什么以及為什么",而不是"如何做"。與將重點放在執行連續命令上的過程性編程相比,函數式編程的重點是函數的定義而不是狀態機(State Machine)的實現。是一種強調表達式的計算而非命令的執行的一種編程風格。表達式是用函數結合基本值構成的,它類似于用參數調用函數(函數式的優美的說明可見《Functional Programming For The Rest of Us》)。
什么是命令式編程?(參考資料:維基百科)
命令式編程,是種描述電腦所需作出的行為的編程典范。幾乎所有電腦的硬體工作都是命令式的;幾乎所有電腦的硬體都是設計來執行機器碼,使用命令式的風格來寫的。較高階的命令式編程語言使用變數和更復雜的語句,但仍依從相同的典范。食譜和行動清單,雖非電腦程式,但與命令式編程有相似的風格:每步都是指令,有形的世界控制情況。因為命令式編程的基礎觀念,不但概念上比較熟悉,而且較容易具體表現于硬體,所以大部分的編程語言都是命令式的。
原文:How Scala Changed My Programming Style 作者:Bill Venners
【相關閱讀】