淺談Java 8的函數語言程式設計

InfoQ - 梅雪松發表於2015-01-21

關於“Java 8為Java帶來了函數語言程式設計”已經有了很多討論,但這句話的真正意義是什麼?

本文將討論函式式,它對一種語言或程式設計方式意味著什麼。在回答“Java 8的函數語言程式設計怎麼樣”之前,我們先看看Java的演變,特別是它的型別系統,我們將看到Java 8的新特性,特別是Lambda表示式如何改變Java的風景,並提供函式式程式設計風格的主要優勢。

函數語言程式設計語言是什麼?

函數語言程式設計語言的核心是它以處理資料的方式處理程式碼。這意味著函式應該是第一等級(First-class)的值,並且能夠被賦值給變數,傳遞給函式等等。

事實上,很多函式式語言比這走得更遠,將計算和演算法看得比它們操作的資料更重要。其中有些語言想分離程式狀態和函式(以一種看起來有點對立的方式,使用物件導向的語言,這通常會將它們聯絡得更緊密)。

Clojure程式語言就是一個這樣的例子,儘管它執行於基於類的Java虛擬機器,Clojure的本質是函式式語言,並且在高階語言源程式中不直接公佈類和物件(儘管提供了與Java良好的互操作性)。

下面顯示的是一個Clojure函式,用於處理日誌,是一等公民(First-class citizen),並且不需要繫結一個類而存在。

(defn build-map-http-entries [log-file]
 (group-by :uri (scan-log-for-http-entries log-file)))

當寫在函式中的程式,對給定的輸入(不論程式中的其它狀態如何)總是返回相同的輸出,並且不會產生其它影響,或者改變任何程式狀態,這時候函數語言程式設計是最有用的。它們的行為與數學函式相同,有時候把遵循這個標準的函式稱為“純”函式。

純函式的巨大好處是它們更容易推論,因為它們的操作不依賴於外部狀態。函式能夠很容易地結合在一起,這在開發者工作流風格中很常見,例如Lisp方言和其它具有強函式傳統的語言中很普遍的REPL(Read, Execute, Print, Loop)風格。

非函數語言程式設計語言中的函數語言程式設計

一種語言是不是函式式並不是非此即彼的狀態,實際上,語言存在於圖譜上。在最末端,基本上是強制函數語言程式設計,通常禁止可變的資料結構。Clojure就是一種不接受可變資料的語言。

不過,也有一些其它語言,通常以函式方式程式設計,但語言並不強制這一點。Scala就是一個例子,它混和了物件導向和函式式語言。允許函式作為值,例如:

val sqFn = (x: Int) => x * x

同時保留與Java非常接近的類和物件語法。

另一個極端,當然,使用完全非函式式語言進行函數語言程式設計是可能的,例如C語言,只要維持好合適的程式設計師準則和慣例。

考慮到這一點,函數語言程式設計應該被看作是有兩個因素的函式,其中一個與程式語言相關,另一個是用該語言編寫的程式:

1)底層程式語言在多大程度上支援,或者強制函數語言程式設計?

2)這個特定的程式如何使用語言提供的函式式特性?它是否避免了非函式式特性,例如可變狀態?

Java的一些歷史

Java是一種固執己見的語言,它具有很好的可讀性,初級程式設計師很容易上手,具有長期穩定性和可支援性。但這些設計決定也付出了一定的代價:冗長的程式碼,型別系統與其它語言相比顯得缺乏彈性。

然而,Java的型別系統已經在演化,雖然在語言的歷史當中相對比較慢。我們來看看這些年來它的一些形式。

Java最初的型別系統

Java最初的型別系統至今已經超過15年了。它簡單而清晰,型別包括引用型別和基本型別。類、介面或者陣列屬於引用型別。

  • 類是Java平臺的核心,類是Java平臺將會載入、或連結的功能的基本單位,所有要執行的程式碼都必須駐留於一個類中。
  • 介面不能直接例項化,而是要通過一個實現了介面API的類。
  • 陣列可以包含基本型別、類的例項或者其它陣列。
  • 基本型別全部由平臺定義,程式設計師不能定義新的基本型別。

從最早開始,Java的型別系統一直堅持很重要的一點,每一種型別都必須有一個可以被引用的名字。這被稱為“標明型別(Nominative typing)”,Java是一種強標明型別語言。

即使是所謂的“匿名內部類”也仍然有型別,程式設計師必須能引用它們,才能實現那些介面型別:

Runnable r = new Runnable() { public void run() { System.out.println("Hello World!"); } };

換種說法,Java中的每個值要麼是基本型別,要麼是某個類的例項。

命名型別(Named Type)的其它選擇

其它語言沒有這麼迷戀命名型別。例如,Java沒有這樣的Scala概念,一個實現(特定簽名的)特定方法的型別。在Scala中,可以這樣寫:

x : {def bar : String}

記住,Scala在右側標示變數型別(冒號後面),所以這讀起來像是“x是一種型別,它有一個方法bar返回String”。我們能用它來定義類似這樣的Scala方法:

def showRefine(x : {def bar : String}) = { print(x.bar) }

然後,如果我們定義一個合適的Scala物件:

object barBell { def bar = "Bell" }

然後呼叫showRefine(barBell),這就是我們期待的事:

showRefine(barBell) Bell

這是一個精化型別(Refinement typing)的例子。從動態語言轉過來的程式設計師可能熟悉“鴨子型別(Duck typing)”。結構精化型別(Structural refinement typing)是類似的,除了鴨子型別(如果它走起來像鴨子,叫起來像鴨子,就可以把它當作鴨子)是執行時型別,而這些結構精化型別作用於編譯時。

在完全支援結構精化型別的語言中,這些精化型別可以用在程式設計師可能期望的任何地方,例如方法引數的型別。而Java,相反地,不支援這樣的型別(除了幾個稍微怪異的邊緣例子)。

Java 5型別系統

Java 5的釋出為型別系統帶來了三個主要新特性,列舉、註解和泛型。

  • 列舉型別(Enum)在某些方面與類相似,但是它的屬性只能是指定數量的例項,每個例項都不同並且在類描述中指定。主要用於“型別安全的常量”,而不是當時普遍使用的小整數常量,列舉構造同時還允許附加的模式,有時候這非常有用。
  • 註解(Annotation)與介面相關,宣告註解的關鍵字是@interface,以@開始表示這是個註解型別。正如名字所建議的,它們用於給Java程式碼元素做註釋,提供附加資訊,但不影響其行為。此前,Java曾使用“標記介面(Marker interface)”來提供這種後設資料的有限形式,但註解被認為更有靈活性。
  • Java泛型提供了引數化型別,其想法是一種型別能扮演其它型別物件的“容器”,無需關心被包含型別的具體細節。裝配到容器中的型別通常稱為型別引數。

Java 5引入的特性中,列舉和註解為引用型別提供了新的形式,這需要編譯器特殊處理,並且有效地從現有型別層級結構分離。

泛型為Java的型別系統增加了顯著額外的複雜性,不僅僅因為它們是純粹的編譯時特性,還要求Java開發人員應注意,編譯時和執行時的型別系統彼此略有不同。

儘管有這些變化,Java仍然保持標明型別。型別名稱現在包括List(讀作:“List-of-String”)和Map, CachedObject>(“Map-of-Class-of-Unknown-Type-to-CachedObject”),但這些仍然是命名的型別,並且每個非基本型別的值仍是某個類的例項。

Java 6和7引入的特性

Java 6基本上是一個效能優化和類庫增強的版本。型別系統的唯一變化是擴大註解角色,釋出可插拔註解處理功能。這對大多數開發者沒有任何影響,Java 6中也沒有真正提供可插拔型別系統。

Java 7的型別系統沒有重大改變。僅有的一些新特性,看起來都很相似:

  • javac編譯器中型別推斷的小改進。
  • 簽名多型性分派(Signature polymorphic dispatch),用於方法控制程式碼(Method handle)的實現細節,而這在Java 8中又反過來用於實現Lambda表示式。
  • Multi-catch提供了一些“代數資料型別”的小跟蹤資訊,但這些完全是javac內部的,對終端使用者程式設計師沒有任何影響。

Java 8的型別系統

縱觀其歷史,Java基本上已經由其型別系統所定義。它是語言的核心,並且嚴格遵守著標明型別。從實際情況來看,Java型別系統在Java 5和7之間沒有太大變化。

乍一看,我們可能期望Java 8改變這種狀況。畢竟,一個簡單的Lambda表示式似乎讓我們移除了標明型別:

() -> { System.out.println("Hello World!"); }

這是個沒有名字、沒有引數的方法,返回void。它仍然是完全靜態型別的,但現在是匿名的。

我們逃脫了名詞的王國?這真的是Java的一種新的型別形式?

也許不幸的是,答案是否定的。JVM上執行的Java和其它語言,非常嚴格地限制在類的概念中。類載入是Java平臺的安全和驗證模式的中心。簡單地說,不通過類來表示一種型別,這是非常非常難的。

Java 8沒有建立新的型別,而是通過編譯器將Lambda表示式自動轉換成一個類的例項。這個類由型別推斷來決定。例如:

Runnable r = () -> { System.out.println("Hello World!"); };

右側的Lambda表示式是個有效的Java 8的值,但其型別是根據左側值推斷的,因此它實際上是Runnable型別的值。需要注意的是,如果沒有正確地使用Lambda表示式,可能會導致編譯器錯誤。即使是引入了Lambda,Java也沒有改變這一點,仍然遵守著標明型別。

Java 8的函數語言程式設計怎麼樣?

最後,讓我們回到本文開頭提出的問題,“Java 8的函數語言程式設計怎麼樣?”

Java 8之前,如果開發者想以函式式風格程式設計,他或她只能使用巢狀型別(通常是匿名內部類)作為函式程式碼的替代。預設的Collection類庫不會為這些程式碼提供任何方便,可變性的魔咒也始終存在。

Java 8的Lambda表示式沒有神奇地轉變成函式式語言。相反,它的作用仍是建立強制的強命名型別語言,但有更好的語法支援Lambda表示式函式文字。與此同時,Collection類庫也得到了增強,允許Java開發人員開始採用簡單的函式式風格(例如filter和map)簡化笨重的程式碼。

Java 8需要引入一些新的型別來表示函式管道的基本構造塊,如java.util.function中的Predicate、Function和Consumer介面。這些新增的功能使Java 8能夠“稍微函數語言程式設計”,但Java需要用型別來表示它們(並且它們位於工具類包,而不是語言核心),這說明標明型別仍然束縛著Java語言,它離純粹的Lisp方言或者其它函式式語言是多麼的遙遠。

除了以上這些,這個函式式語言能量的小集合很可能是所有大多數開發者日常開發所真正需要的。對於高階使用者,還有(JVM或其它平臺)其它語言,並且毫無疑問,將繼續蓬勃發展。

相關文章