Maybe 有無、Either 對錯

February 6, 2022

將程式世界區分為純綷與非純綷的 Haskell,面對錯誤時,也同樣有兩套哲學,其中一套使用 MaybeEither 這類型態來處理錯誤,另一套是 Exception,這篇會先談談 MaybeEither,至於 Exception,之後討論不純的世界時再來說。

Maybe 有無

在〈減輕型態負擔的型態參數〉談過如何定義 Maybe,如果你的查詢或運算,可能會有結果,也可能不會有結果,可以試著讓它傳回 Maybe

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

快速排序發明者、圖靈獎得主Tony Hoare,在 QCon London 2009 主講《Null References: The Billion Dollar Mistake》場次,演講摘要中即指出 null 的使用,已經造成無數的錯誤、弱點與系統當機,在過去四十年來,或許造就了價值數十億美元的苦難與損失。

JSR166 參與者之一 Doug Lea 也曾說過「Null sucks」!

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

在 Haskell 中,面對有無的問題,多半使用 Maybe 解決,這個哲學也影響了不少主流語言,像是 Java 的 Optional、Scala 的 Option 等。

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

ghci> head []
*** Exception: Prelude.head: empty list
ghci>

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

ghci> let x = head []
ghci>

看到了嗎?因為惰性的關係,head [] 並沒有馬上執行,之後的文件會看到,Haskell 還是可以使用 trycatch 之類的函式來處理 Exception,不過這是不純世界的事情。

在純的世界中,處理 list 時,最好總是考慮到空 list,無論是使用判斷式或是模式比對,也許你可以定義一個安全的 head' 函式:

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

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

Nothing 就是沒有,至於為什麼沒有,無法提供任何資訊,另一點是,如果你的應用程式,認為 head 處理空 list 真的是個錯,並且想以純函數式的概念,在純綷的世界中處理時該怎麼辦?

Either 對錯

如果純綷的世界中對執行結果要處理對錯問題,可以使用 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

ghci> let right = Right 10
ghci> right
Right 10
ghci> :t right
right :: Num b => Either a b
ghci> let left = Left "shit happens"
ghci> left
Left "shit happens"
ghci> :t left
left :: Either String b
ghci>

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

head' :: [a] -> Either String a
head' [] = Left "an empty list has no head"
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,這個程式會取得錯誤訊息顯示出來。

在非同步的場合,常見回呼函式有可接收 error 與 success 的參數,某些程度上也有 EitherLeftRight 概念;另外,Go 語言的函式傳回 ok, error 兩個值,讓呼叫者進行檢查,也是類似的風格。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter