Java 開發者的函數式程式設計(4)不可變特性


English

函數式程式設計的重要特性之一就是,變數不可變(Immutable)。技術上來說,純函數式語言沒有變數,例如,如果在 Haskell 寫下 x = 1,那麼就表示 x 代表 1,而不是說 x 目前儲存 1 這個值,而後還可以把它改為別的值之類的,你對 x 能做的事,就是透過 x 名稱來取值。
 可變的(Mutable)變數會有什麼問題嗎?如果程式流程中有可變的變數,因為要改變它們的值太簡單了,反而使得問題難以切割為子問題;使用了非區域變數的方法可能有副作用,也就是說,在給定相同引數的情況下,方法可能傳回不同的結果,因為這些方法有看不到的輸入與輸出;如果物件狀態可變,物件就會是副作用的集合體,因為值域就相當於方法的非區域變數,追蹤變數的難度會提昇至追蹤物件狀態的層級,如果物件是運用在並行的場合,那麼要處理物件狀態的同步問題就會變得困難。
 純函數式語言中,不可變是基本的特性,可強制我們將冗長的程式流程分解為較小的子流程;方法使用了不可變的非區域變數並不會有副作用,物件不會是副作用集合體,也就不會有執行緒同步處理的問題。
 對於習慣命令式風格的程式設計者來說,要想像不可變的變數可能會有點難度,不過也沒有這麼困難。有發現嗎?在先前文章的範例中,我們沒有改變任何一個變數值或物件狀態。我們映射了首元素,然後將尾清單再傳給 map 方法,我們對首元素進行過濾,然後再將尾清單傳給 filter 方法,我們消化首元素,然後將尾清單又傳給 reduce 方法,我們並沒有改變任何變數值或物件狀態。 一旦你無法使用可變的變數,那麼程式流程就會發生變化。 例如,你無法使用迴圈,像是 forwhile 迴圈。迴圈的問題在於,它們天生就具有副作用,迴圈中經常會修改變數值或物件狀態,程式設計者很容易在一個迴圈中修改變數個變數值或物件狀態,因而使得程式流程變得越來越複雜,也就是說,迴圈可能會同時間處理了數個子問題,使得迴圈本身成為邏輯泥塊(Logic clump)。 可以看到,一旦變數不可變,就會強制你將問題分解為子問題,這是因為你無從選擇,不可變特性是個強制找出邏輯泥塊的方式,並使用方法將之提煉出來,因此,你無法使用迴圈,遞迴會是較好的替代方案,你必須使用方法來封裝 if-else 陳述(Statement),因為 if-else 陳述天生就會修改變數。例如,若你撰寫了以下的程式碼...
String nickName = getNickName("Justin");
if(nickName == null) {
    nickName = "Guest";
}
則可以定義一個 getOrElse 方法來封裝 if 陳述:
static String getOrElse(String original, String replacement) {
    if(original == null) {
        return replacement;
    } else {
        return original;
    }
}
那麼,你就可以使用 getOrElse 方法來避免修改變數:
String nickName = getOrElse(getNickName("Justin"), "Guest");
事實上,Java 是有個語法,類似函數式語言中常用的 if-else 運算式(Expression),也就是三元運算子 ?:,雖然一般並不怎麼建議使用。如果真的想使用這個三元運算子的話,可以如下撰寫程式:
String name = getNickName("Justin");
String nickName = (name != null ? name : "Guest");
Java 開發者的函數式程式設計(1) 中,我們看過《Functional Programming for Java Developers in Functional Programming》這本書第一章〈Why Function Programming?〉中列出的要點,因為現在我們已經知道什麼是 代數資料型態,看過幾個 List 處理模式,也知道不可變特性了,現在可以稍微解釋一下那幾個要點的意義:
  • 我想寫好並行程式(Concurrent program)

因為不可變特性,函數式程式設計不會有副作用。

  • 大部份程式只是在做資料處理問題(Data Management Problem)

函數式程式設計定義與使用代數資料型態,代數資料型態易於處理具有規律性的問題,像是資料處理問題。

  • 函數式程式設計更模組化

在進行函數式程式設計時,你必須將問題分解為子問題,一旦有了處理子問題的方案,就可以將這些方案,運用於其它具有相同子問題的問題中。

  • 工作上我要更有效率

因為程式碼變得更簡明,就可以找到更高階的抽象,也就會擁有更多相似問題的解決方案,工作上就會越來越有效率。

  • 函數式程式設計是返樸歸真

一旦熟悉函數式程式設計,該做的事情,就是將問題分解為子問題,幾乎都是這樣。

不過,就如同在 Java 開發者的函數式程式設計(1) 中看過的,如果你使用的語言並非純函數式語言的話,像是 Java,切勿不假思索地直接套用所有函數式的概念。在下一篇的文章中,我們會回到實際的 Java 運用,看看能從函數式程式設計中擷取哪些觀念。