程式扎記: [ Java 文章收集 ] Monad Design Pattern in Java

標籤

2014年11月28日 星期五

[ Java 文章收集 ] Monad Design Pattern in Java

Source From Here 
Preface 
今天跟大家介紹 Monad,這個令人生畏的單字。不過不用怕,我跟大家一樣,都是不懂它後面那複雜的數理,那 Lambda Calculus。儘管如此,我還是能夠在日常的開發裏使用它,從它得到不少好處。本文將透過實作來闡釋 Monad,也會解釋,對於 Java 開發者來說,它到底有什麼幫助。 

Example 1: Optional 
  1. // 從 Account 裏取出居住的城市名稱  
  2. String getCityName(Account account) {  
  3.   if (account != null) {  
  4.     if (account.getAddress() != null) {  
  5.       if (account.getAddress().getCity() != null) {  
  6.         return account.getAddress().getCity().getName();  
  7.       }  
  8.     }  
  9.   }  
  10.   return "Unknown";  
  11. }  
這程式很簡單,就是從 Account 裏逐一取出城市的名稱,為了避免 NullPointerException,我們必須一層一層用 if 檢查 null。這段程式碼有幾個潛在的問題: 
* 重複:不僅僅是 if != null,像是 getAddress() 也出現了三次
* null 檢查容易忘記。現在還好,但當程式碼開始複雜時就很容易出錯

有沒有辦法可以避免這些問題呢?我們先來試試 extract method: 
  1. // 將檢查 null 的邏輯獨立抽出  
  2. // map() 呼叫一個外部的轉換 function, 如果 value 不是 null   
  3. // 的話,它會將 value 轉成 R。  
  4. R map(T value, Function transform) {  
  5.   if (value != null) {  
  6.     return transform.apply(value);  
  7.   }  
  8.   return null;  
  9. }  
  10.   
  11. //用 map() 改寫後  
  12. String getCityName(Account input) {  
  13.   Address address = map(input, account -> account.getAddress());  
  14.   City city = map(address, a -> a.getCity());  
  15.   String name = map(city, c -> c.getName());  
  16.   if (name != null) {  
  17.     return name;  
  18.   }  
  19.   return "Unknown";  
  20. }  
這裏將 null 的檢查抽成獨立的 method map(),它接受一個外來的轉換 Function。在這個例子裡,轉換 Function 可以用來取值。上面的 getCityName 改寫後,你可以看到 map 後面接的都是一個取值的 lambda。 

hmmm.... 這改寫是有去除掉一些重複的程式,但沒有好很多,因為它還是沒有解決 null 容易遺漏的問題。如果真要保證不會遺漏,最好是 compile 時期就能發現。我們不能變更 Java 語言,不過物件導向給了我們自訂型別的能力 -- 型別不對 compile 就不會過

我們繼續重構,這一次抽象化一個特殊的容器 Optional,包裝這個反復出現 null 檢查: 
  1. class Optional {  
  2.     //容器內存著一個值,有時是 null  
  3.     private final Object value;  
  4.   
  5.     Optional(Object value) {  
  6.         this.value = value;  
  7.     }  
  8.   
  9.     // map() 呼叫一個外部的轉換 function, 如果 value 不是 null  
  10.     // 的話,它會將 value 轉成 R,再用新的容器包一次傳出去。  
  11.     Optional map(transfer) {  
  12.         if (value != null) {  
  13.             return new Optional(transfer(value));  
  14.         }  
  15.         return new Optional(null);  
  16.     }  
  17.   
  18.     //方便的 method  
  19.     Object orElse(defaultValue) {  
  20.         return value != null ? value : defaultValue;  
  21.     }  
  22. }  
上面是一個簡單的 Optional 實作,它裡面可以放一個值 value。他也提供一個 map() method,可以安全地將內部的 value 轉換成其他值。有了這個容器,來看看重構後的程式: 
  1. String getCityName2(Account inputAccount) {  
  2.     Optional optAccount = new Optional(inputAccount);  
  3.     Optional optAddress = optAccount.map({account -> account.address});  
  4.     Optional optCity = optAddress.map({address -> address.city});  
  5.     Optional optName = optCity.map({city -> city.getName()});  
  6.     return optName.orElse("Unknown");  
  7. }  
  8.   
  9. account = new Account(address=new Address(city=new City(name="Taipei")))  
  10. account2 = new Account(address=new Address(city=null))  
  11. printf("City name=%s\n", getCityName2(account))  
  12. printf("City name=%s\n", getCityName2(account2))  
現在每一個過渡物件都有用 Optional 包起來了,如果有人用到這些物件,他們必須透過 map() 或是 orElse() 這些保護過的 method 才能取到裡面的值,因此可以避免NullPointerExceptionOptional.map() 設計成回傳 Optional ,因此可以連串的呼叫,我們可以改寫的更簡潔: 
  1. String getCityName3(Account inputAccount) {  
  2.     return new Optional(inputAccount)  
  3.         .map({account -> account.address})  
  4.         .map({address -> address.city})  
  5.         .map({city -> city.name})  
  6.         .orElse("Unknown");  
  7. }  
Optional 這個精心設計的容器,解決了上面提到的問題: 
* 去除了重複:重複的 if null 檢查被封在 map() 裏
* 利用型別的規範,在 compile 時期就能避免遺漏 null 檢查
* 由於抽象成一獨立 class,我們有機會加入 orElse() 這樣好用的 method

目前為止,Optional 這樣的容器,很像我們今天要討論的 Monad 了,讓我們看更多的例子來進一步了解。 

Example 2: Transactional 
  1. def transfer(Account account1, Account account2, int m) {  
  2.     database.beginTransaction(); //開啟資料庫的交易  
  3.   
  4.     try {  
  5.         account1.withdraw(m); //提錢  
  6.         try {  
  7.             account2.deposit(m); //存錢  
  8.         } catch (Exception e) {  
  9.             database.rollback(); //放棄,恢復資料庫  
  10.             return;  
  11.         }  
  12.     } catch (InsufficientBalanceException e) {  
  13.         System.err.printf("\t[Error] %s\n", e)  
  14.         database.rollback(); //放棄,恢復資料庫  
  15.         return;  
  16.     }  
  17.     if (!database.isRollback()) {  
  18.         database.commit(); //最後都沒異常才會進資料庫  
  19.     }  
  20. }  
第二個例子是個典型的銀行轉帳,account1 提領 m 元,再存入 account2。上面的程式有資料庫的操作,當提領錢不夠的話就會 rollback,而存錢時有異常也是。這樣的程式也有類似第一個範例的問題: 
* 程式碼重複,try catch (exception) {rollback} 出現兩次,而且程式很醜
* 接到 exception 一定要作 rollback。但這太容易忘了

我們來套套看剛才 Optional 範例中學到的解法: 
  1. class Transactional {             
  2.     // 資料庫交易開始  
  3.     static Transactional begin() {  
  4.         database.beginTransaction();  
  5.         return new Transactional(TxState.BEGIN);  
  6.     }  
  7.   
  8.     private final TxState txState;  
  9.     static Database database=new Database()  
  10.   
  11.     Transactional(TxState txState) {  
  12.         this.txState = txState;  
  13.     }  
  14.   
  15.     // 這裏會根據傳入的 transform Function 的行為,  
  16.     // 對資料庫做不同的操作  
  17.     Transactional map(transform) {  
  18.         // 如果當前的交易狀態不是已開始,直接跳過  
  19.         if (txState != TxState.BEGIN)   
  20.         {  
  21.             return this;  
  22.         }  
  23.         try {  
  24.             //執行外部的邏輯  
  25.             TxState result = transform(txState);  
  26.             return new Transactional(result);  
  27.         } catch (TransactionException e) {  
  28.             System.err.printf("\t[Error] %s\n", e)  
  29.             database.rollback(); //transform 如果出錯,放棄交易  
  30.             return new Transactional(TxState.ROLLBACK);  
  31.         }  
  32.     }  
  33.   
  34.     // 如果交易的狀態是已經開始,就對資料庫下 commit。  
  35.     // 反之則跳過不做事。  
  36.     Transactional commit() {  
  37.         return map({state ->   
  38.             database.commit();  
  39.             return TxState.COMMIT;  
  40.         });  
  41.     }  
  42. }  
這裏我們設計了一個容器 Transactional,它紀錄目前的資料庫交易的狀態 TxState。而隨著程式的進行,txState 會一直轉換,同時也會對資料庫操作。Transactional.map() 這個 method 裏則包含了 catch 到 exception 後 rollback 的邏輯。靠這個新容器的幫忙,重構後的程式變為: 
  1. void transfer2(Account account1, Account account2, int m) {  
  2.     Transactional.begin()  
  3.             .map({txState ->  
  4.                 account1.withdraw(m);  
  5.                 return txState;  
  6.             })  
  7.             .map({txState ->  
  8.                 account2.deposit(m);  
  9.                 return txState;  
  10.             })  
  11.             .commit();  
  12. }  
重構後的程式變成另一番氣象了,withdraw 和 deposit 都是寫在 map() 的 lambda 裏,巢狀的 try catch 不見了。如果 withdraw(m) throw exception 時,則包含 deposit(m) 的那個 lambda 會直接跳過,而 commit() 也不會做事。你可以花點時間在腦中跑一輪,體會一下這個設計。至於 lambda 裏的 return txState; 可以暫不理會。 

Transactional 這個新容器解決了: 
* 去除重複:重複的 try catch { rollback } 被封在 map() 裏
* 程式更簡潔,比原本醜不拉嘰的 try catch block 好多了
* 利用型別的規範,在 compile 時期避免遺漏 rollback
* 容器內包含的 TxState,它的變更順序有嚴謹的規範 (這個小範例已經帶入 state machine 的觀念了)
* 加入 commit() 這好用的 method

Transactional 這個例子比較複雜,但相對的,套入 容器 的概念後,我們獲得的好處更多,有一種遇強則強的感覺,這種好事在程式中是很少見的。再繼續深入探討之前,我們來整理一下兩個例子裡,它們容器的共同點: 
* 裡面都有個狀態,會隨著 map() 的運算而改變
* 有一個 constructor 直接收一個初始的狀態
* 有一個 map(transform) 的 method,執行外部給的操作,這 method 本身則封裝了運算的邏輯,它幫我們去除了重複的程式。
* map() 也是回傳容器

可以想見 map() 是個關鍵的設計。現在我們知道它可以去掉 if、try catch 這樣的重複結構,不過如果要去除更複雜的結構,我們需要更強大的 flatMap。 

flatMap -展開轉換 
讓我們回到 Optional 的例子,我們剛才有看到取出城市名稱可以連鎖 map() 呼叫,不過隨著程式越寫越多,難免會出現包了兩層 Optional 的情況: 
  1. class Account  
  2. {  
  3.     // get city 太常用所以寫了個可重用的 method  
  4.     public Optional city() {  
  5.         return new Optional(address).map({o->o.city});  
  6.     }  
  7. }  
  8.   
  9. String getCityName4(Account inputAccount) {  
  10.     Optional optAccount = new Optional(inputAccount);  
  11.       
  12.     //想重用 account.city() 結果出現雙層 Optional  
  13.     Optional optOptCity = optAccount.map({account -> account.city()});  
  14.       
  15.     //只好連續 map 兩次硬生生展開 Optional,好噁...  
  16.     Optional optName = optOptCity.map({optCity->optCity.map({o->o.name})})                          
  17.     
  18.     return optName.orElse("Unknown");  
  19. }  
雙層 Optional 太瘋狂了,我們要有人幫我們 展開 (flat) 其中一層,我們來實作一個 flatMap() 吧: 
  1. class Optional {  
  2.     ...  
  3.     Optional flatMap(transform) {  
  4.         if (value == nullreturn new Optional(null);  
  5.         return transform(value);  
  6.     }  
  7.     ...  
  8. }  
  9.   
  10. String getCityName5(Account inputAccount) {  
  11.     // flatMap 那行的 Function generic 是:  
  12.     //   Function>  
  13.     return new Optional(inputAccount)  
  14.         .flatMap({account -> account.city()})  
  15.         .map({city -> city.getName()})  
  16.         .orElse("Unknown");  
  17. }  
好多了!多了 flatMap() 這個 method 後,就可以自由組合,不論 transform 回傳的結果有沒有包著容器。flatMap 相當的強大,我們的容器可以開始處理更複雜的結構,像是 for loop,來看看下一個範例。 

Example 3: Stream 
第三個例子,是收集一群帳號裡的所有台灣電話。因為每個 Account 都有多個電話,所以用巢狀的 for loop 收集 
  1. List taiwanPhoneNumbers(List accounts) {  
  2.     List numbers = new ArrayList<>();  
  3.     for (Account account : accounts) {  
  4.         for (Phone phone : account.getPhones()) {  
  5.             if (phone.getNumber().startsWith("+886")) {  
  6.                 numbers.add(phone.getNumber());  
  7.             }  
  8.         }  
  9.     }  
  10.     return numbers;  
  11. }  
我們來設計一個新容器 Stream 解決這個重複的運算結構: 
  1. class Stream  
  2. {  
  3.     private List values  
  4.     public Stream(List vals){values = vals}  
  5.   
  6.     Stream flatMap(transform)  
  7.     {  
  8.         def results = []  
  9.         for(def value:values)  
  10.         {  
  11.             Stream transformed = transform(value)  
  12.             for(def result:transformed.values)  
  13.             {  
  14.                 results.add(result)  
  15.             }  
  16.         }  
  17.         return new Stream(results)  
  18.     }  
  19.   
  20.     Stream map(transform) {  
  21.         // 注意:這裏只是 flatMap 和建構子的組合  
  22.         return flatMap({value ->  
  23.             new Stream(asList((transform(value))))});  
  24.     }  
  25.   
  26.     // filter 對每個 T 值做判斷,Stream 中只留下判斷為 true 的值  
  27.     Stream filter(predicate) {  
  28.         // // 一樣只是 flatMap 和建構子的組合  
  29.         return flatMap({value ->  
  30.             if (predicate(value)) {  
  31.                 return new Stream(asList(value));  
  32.             } else {  
  33.                 return new Stream(Collections.emptyList());  
  34.             }  
  35.         });  
  36.     }  
  37.   
  38.     List toList(){return new ArrayList(values)}  
  39. }  
這個陽春的 Stream 容器提供了建構子、flatMap()、以及 map() 和 filter() 四個功能。跟前面的範例不一樣,這一次我把主要的運算邏輯放在 flatMap 裏,你可以看出來 map() 和 filter() 其實只是 flatMap 和建構子的衍生物而已。使用新的 Stream 來重構原來的程式會變成: 
  1. List taiwanPhoneNumbers2(List accounts) {  
  2.     return new Stream(accounts)  
  3.         .flatMap({account -> new Stream(account.getPhones())})  
  4.         .map({phone -> phone.getNumber()})  
  5.         .filter({number -> number.startsWith("+886")})  
  6.         .toList();  
  7. }  
重構後,程式的結構變很多,如果你覺得這裏的 flatMap 運用有點匪夷所思,建議你 trace 一下上面程式的執行。這裏特地用簡化過的 Stream 實作來幫助你了解 flatMap 的來龍去脈。套用 Stream 重構後,我們觀察到: 
* 程式變成宣告式的運算:我們只宣告了 要取值, 要留下台灣電話,這讓程式的意圖凸顯。
* 巢狀的for 以及 if 這些干擾讀程式的命令都被消除了
* 因為 Stream 容器,這允許我們加上 filter 這類的高階行為

宣告式不僅程式易讀,也增加了最佳化的可能性 (Java 8 裏真正的 Stream 效能和 for loop 一樣,可以參考 paper) 。 

Monad Design Pattern 
一共舉了三個範例,分別解決不同的運算問題,但是解法都是設計一個狀態的容器,加上 flatMap() method 來接受轉換的函式。具備這樣特徵的容器我們稱之為 Monad: 
  1. class Monad {  
  2.   // 建構子提供狀態的起始值,當然也可以寫成  
  3.   // factory method,意思一樣就行  
  4.   Monad(T state) {...}  
  5.     
  6.   // flatMap() 提供改變狀態,以及  Monad   
  7.   // 的可組合性 (Composibility)  
  8.    Monad flatMap(Function> transform) {  
  9.      // 封裝反覆出現的運算  
  10.      // ...   
  11.   }  
  12.   
  13.   //map() 只是建構子和 flatMap() 的組合,算是 flatMap 的捷徑  
  14.    Monad map(Function transform) {  
  15.      return flatMap(state ->   
  16.           new Monad(transform.apply(state));  
  17.   }  
  18. }  
如果我直接丟這個 Monad 的定義給你,那你看不懂是正常的,不過經過上面範例的洗禮,我相信現在會比較有感覺了。Monad 自然有其數理上的意義,但是對我們 Java 開發者來說,Monad 扮演的反而是個 Design Pattern,是一個我們開發時,時時可以借用的技巧。 

Monad 的適用範圍 
什麼時候適用 Monad 來解決問題呢?從上面範例的推導裏,相信大家已經有點概念了,我們重新整理成比較通用的規則: 
* 你觀察到程式中,有個反覆出現的運算
* 運算常常會出現巢狀的結構
* 運算很容易寫錯,最好可以 compile 時就先抓到
* 運算過程中,某個值的狀態會改變

如果有幾項符合,那麼就可以試試 Monad 來解決。 

Monad 的優點 
同樣,範例中已經展現了 Monad 帶給我們的好處,我們總結一下 
* 去除重複累贅的程式碼
* 將運算結構提升到型別這個層級,型別帶來的好處很多
--- Compile 時就能檢查出來
--- 將那散佈在程式碼各處的運算,集中並凸顯。
--- 封裝底層的實作
* 將 side effect 外包 給 Monad,主程式只有重要的邏輯
* Monad 的 flatMap 是可以組合,連串呼叫的,程式碼易讀性好。
* 允許加上 domain 裏有意義,高階的 method

優點實在很多啊!重新整理一下範例,看得更清楚些: 
 

Monad 的缺點 
Monad 帶來的好處很多,但有光就有影,它最大的缺點是它是侵入式的,一旦開始採用後,你的 API 就會被迫改變: 
  1. //原本 API 很乾淨的:  
  2. City getCity() {...}  
  3.   
  4. //加上 Monad 的保護,API 非改不可,有時候這不是你想要的  
  5. Optional city() {...}  
  6.   
  7. //如果出現了需要混用不同 Monad 的情境,就完了  
  8. //試想,這 API 能看嗎?  
  9. Transactional> tryCreateCity(String cityName) {...}  
當然如果語言本身就支援 Monad,那或許可以避免這樣的缺點。不過如果是在 Java, C#, Javascript 等等 OOP 語言下使用,那麼這個缺點還是會在,套用 Monad Pattern 時能夠避開就避開。 

Example 4: Promise 
希望現在你對 Monad 已經很有感覺了,也不會再遇到別人提到 Monad,而有聽沒有懂。這篇文的最後,我們來挑戰最後一個例子,它的運算結構很複雜 -- 就是惡名昭彰的 callback hell. 下面 crawlPage() 是個抓網頁的程式 -- 它抓個 html 網頁後就存檔,存檔完再寄信通知: 
  1. //抓網頁存檔再寄信通知,連續 callback 的風格  
  2. void crawlPage(String url) {  
  3.   httpClient.getHtml(url, (String html) -> {  
  4.     String fileName = "Page1.html";  
  5.   
  6.     fileUtil.writeFile(fileName, html, (Boolean success) -> {  
  7.       String email = "my@gmail.com";  
  8.   
  9.       nailClient.sendEmail(email, (Boolean sent) -> {  
  10.         System.out.println("result: " + sent);  
  11.       });  
  12.     });  
  13.   });  
  14. }  
  15.   
  16. //三個非同步的工具,它們的 method 都是接 callback:  
  17. class HttpClient {  
  18.   void getHtml(String url, Consumer callback) {  
  19.     //download html text... then invoke callback  
  20.   }  
  21. }  
  22. class FileUtil {  
  23.   void writeFile(String fileName,   
  24.                  String data,   
  25.                  Consumer callback) {  
  26.     //writing data to file... then invoke callback  
  27.   }  
  28. }  
  29. class MailClient {  
  30.   void sendEmail(String email, Consumer callback) {  
  31.     //sending... then invoke callback  
  32.   }  
  33. }  
這個範例比較特別的是 getHtml(),writeFile(),sendEmail() 這三個 method 都是非同步的,它們裡面做完後,才會呼叫傳進的 callback。這樣巢狀 callback 的程式在 javascript 以及 Android 裏都很常見,三層還好,到了五層以上就會瘋了。前面的範例裏,Monad 解決了 if、for、try catch,那重複的 callback,能夠解開嗎? 

Promise Monad 
  1. class Promise {  
  2.   
  3.   private T value;  
  4.   private Function pendingTransform;  
  5.   private Promise chainPromise;  
  6.   
  7.   Promise(T value) {  
  8.     this.value = value;  
  9.   }  
  10.   
  11.   public  Promise map(Function transform) {  
  12.     return flatMap(value -> new Promise<>(transform.apply(value)));  
  13.   }  
  14.   
  15.   public  Promise flatMap(Function> transform) {  
  16.     if (value != null) {  
  17.       return transform.apply(value);  
  18.     }  
  19.     pendingTransform = transform;  
  20.   
  21.     Promise chainPromiseR = new Promise<>(null);  
  22.     this.chainPromise = chainPromiseR;  
  23.     return chainPromiseR;  
  24.   }  
  25.   
  26.   public void complete(T value) {  
  27.     if (pendingTransform == null) {  
  28.        this.value = value;  
  29.        return;  
  30.     }  
  31.     Promise promiseR =   
  32.         (Promise) pendingTransform.apply(value);  
  33.     promiseR.flatMap(nextValue -> {  
  34.       chainPromise.complete(nextValue);  
  35.       return null//end of promise chain  
  36.     });  
  37.   }  
  38. }  
Promise 代表的是對未來的承諾。在使用上你會先得到一個 Promise,裡面裝的東西可能還不存在,而你信任它之後會給你,因此你會先呼叫 Promise 上的 flatMap() 或是 map() ,預先接上 transform Function。等到它的值有了,transform Function 才會真的被呼叫到。 

為了達到上述的需求,Promise 的相較於前面的 Monad,實作上複雜了點。在 flatMap 這個 method 裏,如果值還不存在的話,我們將 transform Function 先保留在pendingTransform 這個欄位,然後做一份居中的 chainPromise 先回傳。之後等值有了之後,complete() 這個 method 會被呼叫,這時才真的呼叫 transform Function,而它的結果再轉交回給 chainPromise。 

雖然我做了簡單的解釋,不過中間的 chainPromise 繞來繞去的,不是很好懂,建議在腦中仔細 trace 一下整個流程。我們來看看利用 Promise 這個 Monad 重構後會是如何: 
  1. class HttpClient {  
  2.   void getHtml(String url, Consumer callback) {  
  3.     //download html text... then invoke callback  
  4.   }  
  5.   //同樣功能的 method,但改成回傳 Promise 的版本  
  6.   Promise getHtml(String url) {  
  7.     Promise promise = new Promise<>(null);  
  8.     //callback 回傳的結果轉交給 promise.complete  
  9.     getHtml(url, html -> promise.complete(html));  
  10.     return promise;  
  11.   }  
  12. }  
  13.   
  14. class FileUtil {  
  15.   void writeFile(String name,   
  16.                  String data,   
  17.                  Consumer callback) {  
  18.     //writing data to file... then invoke callback  
  19.   }  
  20.   Promise writeFile(String name, String data) {  
  21.     Promise promise = new Promise<>(null);  
  22.     //同上,用 method reference 更簡潔  
  23.     writeFile(name, data, promise::complete);  
  24.     return promise;  
  25.   }  
  26. }  
  27.   
  28. class MailClient {  
  29.   void sendEmail(String email, Consumer callback) {  
  30.     //sending... then invoke callback  
  31.   }  
  32.   Promise sendEmail(String email) {  
  33.     Promise promise = new Promise<>(null);  
  34.     sendEmail(email, promise::complete);  
  35.     return promise;  
  36.   }  
  37. }  
我們將原本的三個工具 method 都加上了回傳 Promise 的版本,你可以發現三個的改法幾乎都一樣:先做好一個空的 Promise,再呼叫實際要做的 method,而該 method 的 callback 回來後直接呼叫 promise 的 complete() ,把 callback 的值交給 promise。 

當工具都 Promise 化之後,爬網頁的程式就可以直接串接每個回傳的 Monad,整個平坦化: 
  1. void crawlPage(String url) {  
  2.   httpClient.getHtml(url)  
  3.     .flatMap(html -> fileUtil.writeFile("Page1.html", html))  
  4.     .flatMap(success -> mailClient.sendEmail("my@gmail.com"))  
  5.     .map(emailSent -> {  
  6.       System.out.println("result: " + emailSent);  
  7.       return null;  
  8.     });  
  9. }  
重構後的程式行為跟原本的一樣,flatMap 和 map 接的 transform Function 裡面做的跟之前的 callback 沒兩樣,而且都是事後才會被呼叫。只是 Promise 設計的精巧,讓你可以避免巢狀的 callback。這個例子裡我們也看到了 Monad 對 API 的 侵入。原本那三個工具程式都要 Promise 化,那個抓網頁的程式才能獲得好處! 底下是整個 Promise 使用 Groovy 改寫的完整範例代碼: 
  1. class Promise  
  2. {  
  3.     def value = null  
  4.     def pendingCallback = null  
  5.     Promise chainPromise = null  
  6.       
  7.     Promise(v){value=v}  
  8.       
  9.     Promise map(callback)  
  10.     {  
  11.         return flatMap({value -> new Promise(callback(value))})  
  12.     }  
  13.       
  14.     /** 
  15.      * Register callback and return next Promise whose complete will be called after callback. 
  16.      * @param callback 
  17.      * @return 
  18.      */  
  19.     Promise flatMap(callback)  
  20.     {  
  21.         if (value != null)   
  22.         {  
  23.             return callback(value)  
  24.         }  
  25.         pendingCallback = callback  
  26.   
  27.         Promise chainPromiseR = new Promise(null)  
  28.         this.chainPromise = chainPromiseR  
  29.         return chainPromiseR  
  30.     }  
  31.       
  32.     public void complete(value)  
  33.     {  
  34.         Promise promiseR = (Promise) pendingCallback(value)  
  35.         if(promiseR!=null)  
  36.         {  
  37.             promiseR.flatMap({nextValue ->  
  38.                 chainPromise.complete(nextValue);  
  39.                 return null//end of promise chain  
  40.             })  
  41.         }  
  42.     }  
  43. }  
  44.   
  45. class HttpClient {  
  46.     void getHtml(String url, callback) {  
  47.         //download html text... then invoke callback  
  48.         Thread.start {  
  49.             printf("Download html text... then invoke callback\n")  
  50.             sleep(2000)  
  51.             callback("html body")  
  52.         }  
  53.     }  
  54.       
  55.     //同樣功能的 method,但改成回傳 Promise 的版本  
  56.     Promise getHtml(String url) {  
  57.       Promise promise = new Promise(null);  
  58.       //callback 回傳的結果轉交給 promise.complete  
  59.       getHtml(url, {html -> promise.complete(html)});  
  60.       return promise  
  61.     }  
  62. }  
  63.   
  64. class FileUtil {  
  65.     void writeFile(String name,  
  66.                    String data,  
  67.                    callback) {  
  68.       Thread.start{  
  69.           printf("Write file=${name} with data=${data}...\n")  
  70.           sleep(1000)  
  71.           callback("write file done!")  
  72.       }  
  73.     }  
  74.                      
  75.     Promise writeFile(String name, String data) {  
  76.       Promise promise = new Promise(null);  
  77.       //同上,用 method reference 更簡潔  
  78.       writeFile(name, data, {result->promise.complete(result)});  
  79.       return promise;  
  80.     }  
  81. }  
  82.   
  83. class MailClient {  
  84.     void sendEmail(String email, callback) {  
  85.         //sending... then invoke callback  
  86.         Thread.start{  
  87.             printf("Sending to ${email}... then invoke callback\n")  
  88.             sleep(1000)  
  89.             callback("Done!")  
  90.         }  
  91.     }  
  92.       
  93.     Promise sendEmail(String email) {  
  94.         Promise promise = new Promise(null);  
  95.         sendEmail(email, {result->promise.complete(result)});  
  96.         return promise;  
  97.     }  
  98. }  
  99.   
  100. httpClient = new HttpClient()  
  101. fileUtil = new FileUtil()  
  102. mailClient = new MailClient()  
  103.   
  104. void crawlPage(String url) {  
  105.     httpClient.getHtml(url) // Will return promise object   
  106.       .flatMap({html -> fileUtil.writeFile("Page1.html", html)})  
  107.       .flatMap({success -> mailClient.sendEmail("my@gmail.com")})  
  108.       .map({emailSent ->           
  109.           System.out.println("Result: " + emailSent);  
  110.         return null;  
  111.       });  
  112. }  
  113.   
  114. crawlPage("www.google.com.tw")  
執行結果如下: 
Download html text... then invoke callback
Write file=Page1.html with data=html body...
Sending to my@gmail.com... then invoke callback
Result: Done!

Conclusion 
Promise 這最後一個範例很複雜,但透過實際的實作,可以幫助各位更加了解 Monad 的能力。Promise 連 callback hell 都能征服了,我想幾乎沒有什麼結構難得倒 Monad 的。我希望各位讀完後,未來可以開始用 Monad 這個詞與其他開發者溝通,可以開始識別出某段程式套用了 Monad 來解決。最後更進一步,能夠自己用 Monad 解決問題。Monad 不是什麼很玄的東西,它只是: 將重複的運算結構隱藏,透過凸顯的型別來強化程式 。 

本文的實作範例雖然可以動,但都未達上線使用的標準,只能拿來作教學用途,不要傻傻的直接 copy 來用。JDK8 裏已經有現成的 Optional, Stream, CompletableFuture 可用,不必再發明輪子。參考資料:Mario Fusco 著的 Monadic Java ,本文的所有概念都是從這學來的。

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!