iThome 網站首載:函數式風格錯誤處理
程式人時常要與錯誤搏鬥,然而介紹或探討錯誤處理的文件或書籍,相對來說還是少了許多。由於許多語言都內建了例外(Exception)處理機制,目前大家對以例外機制處理錯誤較為熟悉,然而觀察其它語言典範,從中觀察一些錯誤處理的方法,以不同的習慣來思索,在錯誤處理上可獲得不少的啟發。舉例來說,例外處理多半是命令式風格,想想看函數式的世界中會是怎麼處理例外呢?
- 解決
null
問題的Option
Scala中有個
Option
,guava-libraries中有個Optional
,JDK8中也將有個Optional
類別,它們的存在目的,基本上是為了避免使用null
,以免遭受因為使用了null
,而造成的各種問題。我曾經在專欄〈補救null的策略〉中談過,null
的問題在於語義含糊不清,可以實現速錯(Fail fast)概念,或者是建立有明確語義的型態來取代null
。實際上
Option
的概念來自於函數式。以Haskell為例,它沒有null
的概念,最類似的值是Nothing
,它是Maybe
型態,Maybe
的其它值要由Just
構造。在Haskell中如果函式執行後也許會產生某型態結果,或者是沒有結果,那就會傳回Maybe
型態的值,呼叫函式的客戶端可使用模式比對(Pattern match)在傳回Just
時取得其中的值,在Nothing
時作沒有結果時的處理。Scala中的Option
有兩個子類別Some
與None
,對照來說,Option
相當於Haskell的Maybe
,Some
對應於Just
,None
對應於Nothing
,Scala支援函數式設計,Some
與None
定義為案例類別(Case class),以支援模式比對。觀察Haskell的
Maybe
或是Scala的Option
的使用時機,會發現它們多半用在計算可能失敗而沒有結果的場合,也就是說,Maybe
或Option
常是函數式世界中處理計算失敗或錯誤的方式。現在不少命令式語言都逐漸納入函數式風格,除了一級函式是最顯而易見的函數式特性之外,實際上guava-libraies中納入Optional
的概念也是,只不過在不支援模式比對的Java語言中,會使用Optional
的isPresent
方法來確認是否有包裹值,也就說是,Optional
除了避免null
相關問題外,實際上也給了我們一些以函數式風格處理錯誤的機會。- 分別表現對錯資訊的
Either
在命令式語言逐步結合函數式概念的過程中,常會出現一些讓熟悉命令式風格的程式人困惑之元素。以嘗試融合物件導向及函數式兩種典範的Scala為例,除了
Option
之外,還提供了Either
型態,表面上看來,在函式可能產生兩種型態之一計算結果時,可以令其傳回Either
,實際上Either
多半運用於處理錯誤,它有Right
與Left
兩個子類別,前者是計算成功時用來包裹正確結果(Right
的英文暗示著正確),後者是在計算失敗時用來包括為何計算錯誤的有效訊息。Haskell中也有個Either
型態,分別可使用Right
與Left
兩個值建構式(Value constructor)。同樣地,函數式的Haskell或Scala中,Either
的值都可以進行模式匹配。從Java這類命令式語言中看
Either
,容易有的疑問是「為什麼不直接拋出例外」?拋出例外意謂著客戶端若要處理例外,必須使用try-catch
語法,try-catch
語法容易寫出更動變數值的程式流程,而拋出例外本身就代表著會中斷程式流程,也就是函數有可能不會有傳回值,這不符合函數式風格。Option
的存在解決了部份問題,然而Option
在計算失敗時只傳回None
,如此一來只會知道計算失敗,但不會知道失敗的原因。可以使用
Either
來解決這類問題。以Scala為例,在結合例外型態下,假設計算成功會傳回Int
,計算失敗會以Exception
包裏錯誤訊息,那麼函式傳回值可以是Either[Exception, Int]
,假設現在計算成功得到93,則傳回Right(93)
,否則建立例外物件e
並傳回Left(e)
,如此一來,呼叫函式的客戶端可以模式匹配作相對應的處理,即使客戶端不打算處理例外,也不用撰寫try-catch
,程式依然可以進行,也就是可以延遲例外的處理。有了Either
這類型態,錯誤處理的方式就可以有函數式的可能性,也因此支援Java進行函數式風格撰寫的Functional Java程式庫,亦定義了Either這個型態。- 看看Scala 2.10新增的
Try
在Scala中有趣的一點是,
try-catch
語法是運算式(Expression),也就是try-catch
會有傳回值,這使得在運用try-catch
語法的場合中,依舊可符合函數式風格,因此若察看Either
的API文件時,可以看到以下的範例程式碼:val result: Either[String,Int] =
try { Right(in.toInt) }
catch { case e: Exception => Left(in) }
實際上這是個很常見的模式,因此在Scala 2.10中增加了
Try
類別,可以直接將上例撰寫為val result: Try[Int] = Try(in.toInt)
,如果in.toInt
執行成功,會傳回Success
實例包裏結果,若失敗則以Failure
實例包裏例外物件。呼叫函式的客戶端,同樣可進一步地進行模式匹配。Try
型態的主要作用之一,在於提供比Either
更明確的語意,Try
的兩個子類因而明確地命名為Success
與Failure
;Try
的另一作用是封裝共用的try-catch
處理流程。在Java領域談到封裝try-catch
共用處理流程的實際例子,是Spring的JdbcTemplate
,它封裝了使用JDBC API時繁瑣的try-catch
處理流程,這不單是為了重用流程樣版,也是為了可讀性,在《Clean Code》書中有個小節談到了〈提取Try/Catch區塊〉,也是類似的道理。既然知道了
Try
的作用在提供更明確語意與重用try-catch
流程,要在Java中建立類似的型態基本上就不是問題,雖然try-catch
在Java中不是運算式,但可以封裝在函式中,並以return
來作到類似功能。現階段的Java版本不支援Lambda語法,寫來會比較囉嗦,如果在JDK8中,則可以較簡潔地寫為tryIt(() -> Integer.parseInt(input))
,並令tryIt
傳回Try<Integer>
實例來達到類似功能。- 從多種典範中學習並思考
在過去,程式人習慣使用
null
,後續因為層出不窮的錯誤,以及《Null References: The Billion Dollar Mistake》之類的呼籲,有人開始思考如何不使用null
,因而引入了函數式領域的概念,使用了Option
來讓語意明確,現在不少程式人都知道Option
的概念與使用的時機。隨著命令式語言漸次地引入函數式的元素,程式人也得瞭解更多的函數式概念,方能瞭解這些元素實際的使用時機,就像Either,它不僅僅是用來包裹可能的兩種型態傳回值,否則的話基本上
Map
或其它自訂型態也作得到這點。Either
讓編譯器有機會進行型態檢查,更重要的是它常應用在錯誤處理的場合。程式語言融合多種典範,代表著程式人需要更多的思考方向,就如Option
、Either
、Try
等元素若進入到命令式語言之中,程式人在必要的時候就得改變一下處理錯誤的習慣,嘗試從另一角度來解決問題。