Maybe 有無、Either 對錯
February 6, 2022將程式世界區分為純綷與非純綷的 Haskell,面對錯誤時,也同樣有兩套哲學,其中一套使用 Maybe 與 Either 這類型態來處理錯誤,另一套是 Exception,這篇會先談談 Maybe 與 Either,至於 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 還是可以使用 try、catch 之類的函式來處理 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 的參數,某些程度上也有 Either 的 Left 與 Right 概念;另外,Go 語言的函式傳回 ok, error 兩個值,讓呼叫者進行檢查,也是類似的風格。


