[譯]通往 Java 函數語言程式設計的捷徑

_Fururur發表於2018-06-16

以宣告式的思想在你的 Java 程式中使用函數語言程式設計技術

Java™ 開發人員習慣於面向命令式和麵向物件的程式設計,因為這些特性自 Java 語言首次釋出以來一直受到支援。在 Java 8 中,我們獲得了一組新的強大的函式式特性和語法。函數語言程式設計已經存在了數十年,與物件導向程式設計相比,函數語言程式設計通常更加簡潔和達意,不易出錯,並且更易於並行化。所以有很好的理由將函數語言程式設計特性引入到 Java 程式中。儘管如此,在使用函式式特性進行程式設計時,就如何設計你的程式碼這一點上需要進行一些改變。

關於本文

Java 8 是 Java 語言自誕生以來最重要的更新,它包含如此多的新特性,以至於你可能想知道應該從哪開始瞭解它。在本系列中,身為作家和教育家的 Venkat Subramaniam 提供了一種符合 Java 語言習慣的 Java 8 學習方式。邀請你進行簡短的探索後,重新思考你認為理所當然的 Java 一貫用法和規範,同時逐漸將新技術和語法整合到你的程式中去。

我認為,以宣告式的思想而不是命令式的思想來程式設計,可以更加輕鬆地向更加函式化的程式設計風格過渡。在 Java 8 idioms series 這個系列的第一篇文章中,我解釋了命令式、宣告式和函數語言程式設計風格之間的異同。然後,我將向你展示如何使用宣告式的思想逐漸將函數語言程式設計技術整合到你的 Java 程式中。

命令式風格(程式導向)

受指令式程式設計風格訓練的開發者習慣於告訴程式需要做什麼以及如何去做。這裡是一個簡單的例子:

清單 1. 以命令式風格編寫的 findNemo 方法
import java.util.*;

public class FindNemo {
  public static void main(String[] args) {
    List<String> names = 
      Arrays.asList("Dory", "Gill", "Bruce", "Nemo", "Darla", "Marlin", "Jacques");

    findNemo(names);
  }                 
  
  public static void findNemo(List<String> names) {
    boolean found = false;
    for(String name : names) {
      if(name.equals("Nemo")) {
        found = true;
        break;
      }
    }
    
    if(found)
      System.out.println("Found Nemo");
    else
      System.out.println("Sorry, Nemo not found");
  }
}
複製程式碼

方法 findNemo() 首先初始化一個可變變數 flag,也稱為垃圾變數(garbage variable)。開發者經常會給予某些變數一個臨時性的名字,例如 fttemp 以表明它們根本不應該存在。在本例中,這些變數應該被命名為 found

接下來,程式會迴圈遍歷給定的 names 列表,每次都會判斷當前遍歷的值是否和待匹配值相同。在這個例子中,待匹配值為 Nemo,如果遍歷到的值匹配,程式會將標誌位設為 true,並執行流程控制語句 "break" 跳出迴圈。

這是對於廣大 Java 開發者最熟悉的程式設計風格 —— 命令式風格的程式,因此你可以定義程式的每一步:你告訴程式遍歷每一個元素,和待匹配值進行比較,設定標誌位,以及跳出迴圈。指令式程式設計風格讓你可以完全控制程式,有的時候這是一件好事。但是,換個角度來看,你做了很多機器可以獨立完成的工作,這勢必導致生產力下降。因此,有的時候,你可以通過少做事來提高生產力。

宣告式風格

宣告式程式設計意味著你仍然需要告訴程式需要做什麼,但是你可以將實現細節留給底層函式庫。讓我們看看使用宣告式程式設計風格重寫清單 1 中的 findNemo 方法時會發生什麼:

清單 2. 以宣告式風格編寫的 findNemo 方法
public static void findNemo(List<String> names) {
  if(names.contains("Nemo"))
    System.out.println("Found Nemo");
  else
    System.out.println("Sorry, Nemo not found");
}
複製程式碼

首先需要注意的是,此版本中沒有任何垃圾變數。你也不需要在遍歷集合中浪費精力。相反,你只需要使用內建的 contains() 方法來完成這項工作。你仍然要告訴程式需要做什麼,集合中是否包含我們正在尋找的值,但此時你已經將細節交給底層的方法來實現了。

在指令式程式設計風格的例子中,你控制了遍歷的流程,程式可以完全按照指令進行;在宣告式的例子中,只要程式能夠完成工作,你完全不需要關注它是如何工作的。contains() 方法的實現可能會有所不同,但只要結果符合你的期望,你就會對此感到滿意。更少的工作能夠得到相同的結果。

訓練自己以宣告式的程式設計風格來進行思考將更加輕鬆地向更加函式化的程式設計風格過渡。原因在於,函數語言程式設計風格是建立在宣告式風格之上的。宣告式風格的思維可以讓你逐漸從指令式程式設計轉換到函數語言程式設計。

函數語言程式設計風格

雖然函式式風格的程式設計總是宣告式的,但是簡單地使用宣告式風格程式設計並不等同與函數語言程式設計。這是因為函數語言程式設計時將宣告式程式設計和高階函式結合在了一起。圖 1 顯示了命令式,宣告式和函數語言程式設計風格之間的關係。

圖 1. 命令式、宣告式和函數語言程式設計風格之間的關係

A logic diagram showing how the imperative, declarative, and functional programming styles differ and overlap.

Java 中的高階函式

在 Java 中,你可以將物件傳遞給方法,在方法中建立物件,也可以從方法中返回物件。同時你也可以用函式做相同的事情。也就是說,你可以將函式傳遞給方法,在方法中建立函式,也可以從方法中返回函式。

在這種情況下,方法是類的一部分(靜態或例項),但是函式可以是方法的一部分,並且不能有意地與類或例項相關聯。一個可以接收、建立、或者返回函式的方法或函式稱之為高階函式

一個函數語言程式設計的例子

採用新的程式設計風格需要改變你對程式的看法。這是一個從簡單例子的練習開始,到構建更加複雜程式的過程。

清單 3. 指令式程式設計風格下的 Map
import java.util.*;

public class UseMap {
  public static void main(String[] args) {
    Map<String, Integer> pageVisits = new HashMap<>();            
    
    String page = "https://agiledeveloper.com";
    
    incrementPageVisit(pageVisits, page);
    incrementPageVisit(pageVisits, page);
    
    System.out.println(pageVisits.get(page));
  }
  
  public static void incrementPageVisit(Map<String, Integer> pageVisits, String page) {
    if(!pageVisits.containsKey(page)) {
       pageVisits.put(page, 0);
    }
    
    pageVisits.put(page, pageVisits.get(page) + 1);
  }
}
複製程式碼

清單 3 中,main() 函式建立了一個 HashMap 來儲存網站訪問次數。同時,incrementPageVisit() 方法增加了每次訪問給定頁面的計數。我們將聚焦此方法。

以指令式程式設計風格寫的 incrementPageVisit() 方法:它的工作是為給定頁面增加一個計數,並儲存在 Map 中。該方法不知道給定頁面是否已經有計數值,所以會先檢查計數值是否存在,如果不存在,會為該頁面插入一個值為"0"的計數值。然後再獲取該計數值,遞增它,並將新的計數值儲存在 Map 中。

以宣告式的方式思考需要你將方法的設計從 "how" 轉變到 "what"。當 incrementPageVisit() 方法被呼叫時,你需要將給定的頁面計數值初始化為 1 或者計數值加 1。這就是 what

因為你是通過宣告式程式設計的,那麼下一步就是在 JDK 庫中尋找可以完成這項工作且實現了 Map 介面的方法。換言之,你需要找到一個知道如何完成你指定任務的內建方法。

事實證明 merge() 方法非常適合你的而目的。清單 4 使用新的宣告式方法對清單 3 中的 incrementPageVisit() 方法進行修改。但是,在這種情況下,你不僅僅只是選擇更智慧的方法來寫出更具宣告性風格的程式碼,因為 merge() 是一個更高階的函式。所以說,新的程式碼實際上是一個體現函式式風格的很好的例子:

清單 4. 函數語言程式設計風格下的 Map
public static void incrementPageVisit(Map<String, Integer> pageVisits, String page) {
    pageVisits.merge(page, 1, (oldValue, value) -> oldValue + value); 
}
複製程式碼

在清單 4 中,page 作為第一個引數傳遞給 merge():map 中鍵對應的值將會被更新。第二個引數作為初始值,如果 Map 中不存在指定鍵的值,那麼該值將會賦值給 Map 中鍵對應的值(在本例中為"1")。第三個引數為一個 lambda 表示式,接受當前 Map 中鍵對應的值和該函式中第二個引數對應的值作為引數。lambda 表示式返回其引數的總和,實際上增加了計數值。(編者注:感謝 István Kovács 修正了程式碼錯誤)

清單 4incrementPageVisit() 方法中的單行程式碼與清單 3 中的多行程式碼進行比較。雖然清單 4 中的程式是函數語言程式設計風格的一個例子,但通過宣告性地思想去思考問題幫助能夠我們實現飛躍。

總結

在 Java 程式中採用函數語言程式設計技術和語法有很多好處:程式碼更簡潔,更富有表現力,移動部分更少,實現並行化更容易,並且通常比物件導向的程式碼更易理解。 目前面臨的挑戰是,如何將你的思維從絕大多數開發人員所熟悉的指令式程式設計風格轉變為以宣告式的方式進行思考。

雖然函數語言程式設計並沒有那麼簡單或直接,但是你可以學習專注於你希望程式做什麼而不是如何做這件事,來取得巨大的飛躍。通過允許底層函式庫管理執行,你將逐漸直觀地瞭解用於構建函數語言程式設計模組的高階函式。

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章