Functional Programming中處理Exception的方法
前言
以前剛從Java轉換到其他語言時有個困擾,就是Checked Exception(受檢例外)不見了。這會導致呼叫function的時候,難以判斷程式到底會不會丟出Exception。
其實關於Checked Exception的討論在網路上已經很多了,我google了兩篇覺得寫得還不錯的,覺得應該存在的正方[1],覺得不應該存在的反方[2],講起來其實都有些道理。
我自己比較偏向Checked Exception必須存在,但更準確來說是「我自己的checked exception應該要存在」。在設計自己的function時,若出現需要raise exception的情況,必定是我覺得這個東西要知會上層中斷流程,並做出一定的處理。而這應該存在於function的interface當中,不該讓同事看了文件或看了原始碼才發現要處理例外。
至於其他library的程式碼,我是覺得隨意。一方面大多數library並不會隨便丟出錯誤。另一方面就算他丟出錯誤,很大部分也是無法處理的錯誤,就讓他直接丟到頂端被top level的try catch給接住也無妨。
嘗試: 回歸錯誤碼檢查
既然想讓exception在function的介面上展現,最簡單的一個方法就是用class給包裝起來。(下面用dart程式碼呈現)
class Result{
Result(this.result);
Result.error(this.error);
int result;
Exception error;
}
先宣告一個class,裡面有正常的回傳值,也有異常的回傳值。
若程式正常執行則使用預設的constructor,出現錯誤則用error的constructor。如下
Result func(){
return Result(0);
// return Result(Exception("Error!!!"));
}
void main(){
Result v = func();
if(v.error != null){
// 處理錯誤
return;
}
}
在程式的執行流程中,如果出現error != null的狀況,就進行處理。若有需要也可以再將exception向上傳遞。
這邊為了易懂省掉了generic type的設定,實務上為了更一般化一定要用上generic type來包裝。
缺點
缺點也是顯而易見,exception受歡迎的原因就是因為它可以中斷程式流程,並把錯誤集中處理。若按照這種寫法,很快的程式中就會開始充斥著類似下方的程式碼。
void main(){
Result a = func1();
if (a.error != null){
// 處理
}
Result b = func2();
if (b.error != null){
// 處理
}
Result c = func3();
if(c.error != null){
// 處理
}
}
所有錯誤處理分散在function的各個地方,程式流程也變得非常不線性,看一段錯誤處理又要看一段正常流程,相信許多人會因此感到痛苦。
Functional Programming下的處理方法
型態修改
在正式介紹FP的處理法之前,先把上面的型態宣告做點小修改。
class Result{}
class ResultFailed extends Result {
ResultFailed(this.e);
Exception e;
}
class ResultSuccess extends Result {
ResultSuccess(this.value);
int value;
}
可以想見,一個Exception跟一個正常值,根本不會是同一種type,所以用一個Result來裝兩者,在語意上其實是有點問題的。
此處先將Result作為Base Class,並另外分出兩種class來記錄正確和錯誤。
串接寫法
剛剛程式中最大的問題出在無法將錯誤集中處理,現在試著幫Result加上一個function來解決這個問題。
// Transform是一個parameter為int,return type為int的function。
typedef Transform = int Function(int value);
class Result{
Result map(Transform fn){
if (this is ResultException){
return this;
}else if (this is ResultSuccess){
return ResultSuccess(fn(a));
}
}
}
Result func(){
return ResultSuccess(0);
// return ResultFailed(Exception("NOOO"));
}
void main(){
Result value = func()
.map((int v) => v+10)
.map((int v) => v+10)
.map((int v) => v+10);
if (value is ResultFailed){
// 處理錯誤
}
}
先集中精力看main function的部分,分析一下map造成了什麼效果?
在執行func獲得一個Result之後,會想拿這個Result去做去執行其他運算,例如說+10。但是在此處我們把「從Result class中提取int值」的動作交給了map,map再去呼叫使用者所傳入的function。
這有什麼好處?
回頭看看map的實作,會發現map除了在程式正確執行的時候,幫助unwrap int值,也能在程式錯誤的時候,選擇性的不執行傳進來的function。因此可以百分之百肯定,當執行function時,參數v鐵定是有值,且沒有exception存在。
要再functional一點的話,應該在Result上再外加一個handleError的function。
因此我們可以這樣描述main的執行
- 如果func()執行成功,沒有丟出exception,那麼每一個map都會被執行,最終會得到一個ResultSuccess(30)的結果。
- 如果func()執行失敗,丟出了Exception(“NOOO”),那麼接下來的每一步運算都不會被執行。最後的結果還是Exception(“NOOO”)。
而在這樣的寫法中,正確的計算步驟被層層串接,錯誤的處理則被推遲到最後才執行。就跟try catch想達到的效果是一樣的。
另外一方面,如果不想在當前的function處理錯誤,就可以將function的return type換成Result,這樣所有使用此function的人,都會強制注意到此function是有可能回傳Exception的,進而選擇執行錯誤處理,或是將Exception再遞交給更上一層。這跟Java中Checked Exception想達成的效果也是一樣的。
已經存在的輪子
作為一個簡化的例子,這個Result class其實有很多缺陷。不過幸好在FP的世界裡這樣的設計已經有自己的名稱,叫做Either,且很多程式語言都已經有實作,所以實務上並不需要自己重刻一次這樣的工具。
這裡貼幾個Either的實作
Either的繼承樹長得像這樣。
class Either<L,R> {}
class Left<L,R> extends Either<L,R>{}
class Right<L,R> extends Either<L,R>{}
Either的設計其實更為一般化,並沒有規定Left和Right必須要是一個Exception和一個正確值。但在使用慣例上,Left是作為Exception存在,Right作為正確值存在。最好是跟隨這個慣例,並避免將Either用在非Exception處理的地方。
留言
張貼留言