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 兩個值,讓呼叫者進行檢查,也是類似的風格。