java8函數語言程式設計筆記-破壞式更新和函式式更新

dust1發表於2018-09-26

破壞式更新和函式式更新

什麼是破壞式更新和函式式更新:

破壞式更新:
  有一個方法,傳入一個物件並返回結果。在方法結束之後傳入的引數物件也被改變了,這就是破壞式更新。你不能保證呼叫這個方法之後後續是否還會使用傳入的引數物件,因此破壞式更新在java的函數語言程式設計中是不被提倡的。這也是另一種副作用。
函式式更新:
  用函數語言程式設計的方法解決問題,強調沒有任何副作用
破壞式更新例子:

我們有一個類用來儲存火車站點的票務資訊(利用簡單的單向連結串列),表示從A地到B地的火車旅行,旅途中我們需要換車,所以需要使用幾個由onward欄位串聯在一起的TrainJourney物件,直達火車或者旅途的最後一段onward為null

class TrainJourney {
    public int price;
    public TrainJourney onward;
    public TrainJourney (int price, TrainJourney t) {
        this.price = price;
        this.onward = t;
    }
}
複製程式碼

假設我們有幾個互相分隔的TrainJourney物件分別代表A到B,B到C的旅行。我們希望建立一段新的旅行,它能將兩個TrainJourney物件串聯起來(即從A到B到C) 首先我們採用的是傳統命令式的方法:

 public static TrainJourney link (TrainJourney a, TrainJourney b) {
     if (a == null) {
         return b;
     }
     TrainJourney t = a;
     while (a.onward != null) {
         t = a.onward;
     }
     t.onward = b;
     return a;
 }
複製程式碼

這個方法具體的執行應該不用多講了,這裡我們注意到的是t.onward = b;這個操作之後return的還是a,這就出現了一個問題,這裡我們進行的操作是直接修改了引數a,也就是引數a在執行完這個方法之後原來的資料結構就被改變了。如果我們還用引數a,b傳入這個方法,返回的資料和第一次便不一樣了,這樣就產生了副作用。這個缺陷我們需要克服。因此:

如果我們需要使用表示計算結果的資料結果,那麼請建立它的一個副本而不要直接修改現存的資料結構。這個最佳的實踐也適用於標準的物件導向程式設計。

public static TrainJourney link (TrainJourney a, TrainJourney b) {
       if (a == null) {
           return b;
       }
       TrainJourney t = new TrainJourney(a.price, a.onward);
       TrainJourney t1 = a;
       TrainJourney t2 = t;
       while (t1.onward != null) {
           t2.onward = new TrainJourney(t1.onward.price, t1.onward.onward);
           t2 = t2.onward;
           t1 = t1.onward;
       }
       t2.onward = b;
       return t;
}
複製程式碼

上述程式碼就是我們修改之後的程式碼,但是我們可以看到while語句中多次使用了new關鍵字建立物件來複制連結串列。但是這種方法會導致過度的物件複製。這時候,如果我們採用函數語言程式設計的方法:

public static TrainJourney append (TrainJourney a, TrainJourney b) {
    return a == null ? b : new TrainJourney (a.price, append(a.onward, b));
}
複製程式碼

和上面一對比,函數語言程式設計的優點顯而易見

  • 程式碼量大大減少
  • 沒有物件複製導致的開支,執行速度快

函數語言程式設計的程式碼一大特點就是我們只需要編寫操作的步驟(先做什麼,後做什麼),具體如何操作(先做什麼的具體操作)不需要我們寫。在上述的例子中,我們從程式碼能看到,我們先檢查引數a是否為空,如果為空則返回b,如果不為空則返回一個新的TrainJourney物件,這個物件的票價是引數a的票價,onward為遞迴呼叫append函式返回的值,遞迴時的引數為引數a的onward和引數b,說起來很繞。我簡單地理解為:

函數語言程式設計的程式碼只保留流程,具體操作全部交給程式自行完成。是一個偷懶的過程

這段程式碼有一個特別的地方,它並未建立整個新 TrainJourney物件的副本——如果a是n個元素的序列,b是m個元素的序列,那麼呼叫這個函 數後,它返回的是一個由n+m個元素組成的序列,這個序列的前n個元素是新建立的,而後m個元 素和TrainJourney物件b是共享的。

java8函數語言程式設計筆記-破壞式更新和函式式更新

另一個例子:
先前我們使用的是連結串列的例子,現在我們試試其他資料格式,最常見的就是二叉樹

class Tree {
    public String key;
    public int val;
    public Tree left, right;
    
    public Tree (String key, int newval, Tree l, Tree r) {
        this.key = key;
        this.val = newval;
        this.left = l;
        this.right = r;
    }
}
複製程式碼

這時候,我們希望根據key更新二叉樹的val,一般的寫法如下:

public static Tree update (String key, int newval, Tree t) {
    if (t == null) {
       t = new Tree(key, newval, null, null);
    } else if (key.equals(t.key)) {
        t.val = newval;
    } else if (key.compareTo(t.key) < 0) {
        t.left = update (key, newval, t.left);
    } else {
        t.right = update (key, newval, t.right);
    }
}
複製程式碼

但是這種方法都會對現有的樹進行修改,這意味著使用樹存放對映關係的所有使用者都會感知到這些修改,即破壞了原來的資料結構。 那麼函數語言程式設計是怎麼樣的呢?

public static Tree append (String k, int newval, Tree t) {
    return t == null ? new Tree (key, newval, null, null) :
       k.equals(t.key) ?
           new Tree (k, newval, t.left, t.right) : 
           k.compareTo(t.key) < 0 ?
               new Tree (k, newval, append (k, newval, t.left), t.right) : 
               new Tree (k, newval, t.left, append (k, newval, t.right));
}
複製程式碼

這段程式碼中,我們只用一行語句進行條件判斷,沒有采用if-else-then是為了強調,該寫法沒有任何副作用。不過如果採用if-else-then語句也可以,在每一個條件判斷之後都加上return.

java8函數語言程式設計筆記-破壞式更新和函式式更新

相關文章