Haskell Tutorial(22)Maybe 有無、Either 對錯


將程式世界區分為純綷與非純綷的 Haskell,面對錯誤時,也同樣有兩套哲學,其中一套使用 MaybeEither 這類型態來處理錯誤,另一套是 Exception,習慣 C++、Java 的人,第一次看到後者可能會感到欣慰,不過,並不是大家想的那樣。

這篇會先談談 MaybeEither,以及一點點的 Exception 作為面對錯誤的開始 …

Maybe 有、Maybe 無

之前曾多次看過 Maybe,像是〈Haskell Tutorial(14)減輕型態負擔的型態參數〉中,就談過如何定義 Maybe,如果你的查詢或運算,可能會有結果,也可能不會有結果,就可以試著讓它傳回 Maybe

「無」、「沒有」、「空」這類概念,開發者經常沒有明確定義出來,舉 Java 為例好了,查詢一筆資料時若沒有結果,是要傳回空清單還是 null?查不到指定的客戶名稱時,是要傳回 "" 還是 null?查不到指定的訂單時,是要傳回 NullOrder 還是 null,又或者,這些乾脆都拋出 Exception?「無」、「沒有」、「空」是一種錯誤嗎?是可處理的錯誤或是 Bug?

Maybe 重要性之一,是將「無」、「沒有」、「空」這類概念,使用 Nothing 明確定義出來,因為傳回型態是 Maybe Something,你不能傳回別的東西,要不就 Nothing,要不就 Just Something,型態不符的錯誤是會被編譯器揪出來的,而開發者在取得 Maybe 型態的傳回值之後,也會知道要使用模式比對看看是 Nothing,或者是取得 Something,這是最好的方式。

因此,在 Haskell 中,面對有無的問題,多半使用 Maybe 解決,這個哲學也影響了不少主流語言,像是 Java 8 的 Optional、Scala 的 Option,甚至 Apple 在 WWDC2014 發表的 Swift 語言中,也有這類概念。

那麼剩下的,就是面對「無」、「沒有」、「空」是否為錯誤、是可處理的錯誤或是 Bug 這類的問題,舉例來說,head 函式可以對 List 取首元素,那麼對 head [] 會如何?

head

喔喔!head 噴出 Exception,這表示它認為對一個空 List 取首元素是個錯誤,因為空 List 沒有頭啊!只是這有兩個問題,第一,你的應用程式中也許不認為這是個錯誤,只要表明空 List 「沒有」頭就好了,第二個問題比較嚴重,因為惰性的關係,你不會知道何時會噴出了 Exception,這麼一來,處理 Exception 的時機就是個大麻煩,例如:

head

看到了嗎?因為惰性的關係,head [] 並沒有馬上執行,之後會看到,在 Haskell 是可以使用 trycatch 之類的函式來處理 Exception,不過,因為惰性的關係,對純綷世界的函式,你很難掌握使用 trycatch 的時機。

這就是為什麼,在處理 List 時,最好總是考慮到空 List,無論你是使用判斷式或者是模式比對,也許你也可以定義一個安全的 head' 函式:

head' :: [a] -> Maybe a
head' [] = Nothing
head' (x:xs) = Just x

這個 head' 在面對 [] 時,會傳回 Nothing,而不是噴出一個錯誤,對純綷世界的函式來說,呼叫 head' 會比較好處理的多。

Nothing 就是沒有,至於為什麼沒有,無法提供任何資訊,另一點是,如果你的程式中,認為 head 處理空 List 真的是個錯怎麼辦?

Either 對錯

如果對執行結果要處理對與錯的問題,又不想面對 Exception,那麼可以使用 Either,它的定義像是:

data Either a b = Left a | Right b deriving (Eq, Ord, Read, Show)

每次看到 Either 的值建構式,總會讓我想起一句好笑的句子:

Your left brain has nothing right, and your right brain has nothing left!

這句話總能很快地讓我記起 Either 的作用,如果處理問題有正確的結果,那就使用 Right,如果發生錯誤,那就使用 Left

Either

可以從 Either 中看到,值建構式是可以部份套用的,那麼,要怎麼使用 Either 來表示對空 List 取首元素的錯誤呢?

head' :: [a] -> Either String a
head' [] = Left "an empty list has no head .. XD"
head' (x:xs) = Right x

這個 head' 使用字串來描述錯誤,實際上,也可以使用 Exception 描述,這之後會看到,來看看怎麼使用這個 head'

import Data.List

head' :: [a] -> Either String a
head' [] = Left "an empty list has no head .. XD"
head' (x:xs) = Right x

main = do
    case (head' . sort) [1, 5, 3, 2, 4] of 
        Left err -> putStrLn err
        Right h  -> print (h * 10)

這個程式假設 List 是某個函式產生的,也有可能是空 List,對於產生的 List 會先進行排序(sort[] 只會產生 []),然後取首元素乘上 10,如果真的產生了空 List,這個程式會取得錯誤訊息顯示出來。

一點點的 try

好吧!如果真的要處理 headException,該怎麼做呢?這邊先稍微使用一下 Control.Exception 中的 try 函式好了!

import Control.Exception

main = do
    result <- try (return $ head [1, 2, 3])
    case result :: Either SomeException Int of
        Left  ex -> putStrLn $ "發生 Exception:" ++ show ex
        Right ele -> putStrLn $ "答案:" ++ show ele

try 的型態是 Exception e => IO a -> IO (Either e a),它接受一個 IO a 的型態,因此 head 的執行結果使用 return 傳回一個 IO Int,作為 try 的引數,try 傳回一個 IO (Either e a),正確執行的話結果就是 a,發生錯誤的話,會捕捉 Exceptione,在這邊看到的 SomeException,具有 Exception 的行為,先用簡單的說法解釋的話,SomeException 為具有 Exception 行為的頂層型態(想瞭解階層系統如何定義,可以參考 The Exception type)。

如果執行上面這個程式,結果會顯示 "答案:1",不過,試著將 [1, 2, 3] 改為 [] 並編譯執行,你會看到什麼?"發生 Exception:empty list"?不是!你會看到 "Prelude.head: empty list",這是 Haskell 執行環境給你的訊息,不是你定義出來要顯示的訊息。

這是因為惰性的關係,head [] 執行的時機並不一定在 try 中而導致,Haskell 有個 evaluate 函式,可以用來要求立即執行函式:

import Control.Exception

main = do
    result <- try (evaluate (head []))
    case result :: Either SomeException Int of
        Left  ex -> putStrLn $ "發生 Exception:" ++ show ex
        Right ele -> putStrLn $ "答案:" ++ show ele

編譯執行這個函式,你才會看到 "發生 Exception:Prelude.head: empty list" 的顯示結果,這表示,從純綷的函式中拋出 Exception 並不是個好主意,因此在 Haskell 的純綷世界中,不建議從函式中拋出 Exception(之後會談到如何拋出 Exception),儘量使用 Maybe 來表示有無,或者是使用 Either 來表示對錯,至於 Exception,建議在非純綷的世界中使用,像是輸入輸出,必然就是非純綷的,而且總是有意外(指定的檔案不存在之類的)。