無(wú)論使用哪門(mén)語(yǔ)言,錯(cuò)誤處理都是程序員最重要–也是最容易忽視–的話題之一。在Haskell中,你會(huì)發(fā)現(xiàn)有兩類(lèi)主流的錯(cuò)誤處理:“純”的錯(cuò)誤處理和異常。
當(dāng)我們說(shuō)“純”的錯(cuò)誤處理,我們是指算法不依賴(lài)任何IO Monad。我們通常會(huì)利用Haskell富于表現(xiàn)力的數(shù)據(jù)類(lèi)型系統(tǒng)來(lái)實(shí)現(xiàn)這一類(lèi)錯(cuò)誤處理。Haskell也支持異常。由于惰性求值復(fù)雜性,Haskell中任何地方都可能拋出異常,但是只會(huì)在IO monad中被捕獲。在這一章中,這兩類(lèi)錯(cuò)誤處理我們都會(huì)考慮。
讓我們從一個(gè)非常簡(jiǎn)單的函數(shù)來(lái)開(kāi)始我們關(guān)于錯(cuò)誤處理的討論。假設(shè)我們希望對(duì)一系列的數(shù)字執(zhí)行除法運(yùn)算。分子是常數(shù),但是分母是變化的。可能我們會(huì)寫(xiě)出這樣一個(gè)函數(shù):
-- file: ch19/divby1.hs
divBy :: Integral a => a -> [a] -> [a]
divBy numerator = map (numerator `div`)
非常簡(jiǎn)單,對(duì)吧?我們可以在 ghci 中執(zhí)行這些代碼:
ghci> divBy 50 [1,2,5,8,10]
[50,25,10,6,5]
ghci> take 5 (divBy 100 [1..])
[100,50,33,25,20]
這個(gè)行為跟我們預(yù)期的是一致的:50 / 1 得到50,50 / 2 得到25,等等。甚至對(duì)于無(wú)窮的鏈表 [1..] 它也是可以工作的。如果有個(gè)0溜進(jìn)去我們的鏈表中了,會(huì)發(fā)生什么事呢?
ghci> divBy 50 [1,2,0,8,10]
[50,25,*** Exception: divide by zero
是不是很有意思? ghci 開(kāi)始顯示輸出,然后當(dāng)它遇到零時(shí)發(fā)生了一個(gè)異常停止了。這是惰性求值的作用–它只按需求值。
在這一章里接下來(lái)我們會(huì)看到,缺乏一個(gè)明確的異常處理時(shí),這個(gè)異常會(huì)使程序崩潰。這當(dāng)然不是我們想要的,所以讓我們思考一下更好的方式來(lái)表征這個(gè)純函數(shù)中的錯(cuò)誤。
可以立刻想到的一個(gè)表示失敗的簡(jiǎn)單的方法是使用 Maybe 。如果輸入鏈表中任何地方包含了零,相對(duì)于僅僅返回一個(gè)鏈表并在失敗的時(shí)候拋出異常,我們可以返回 Nothing ,或者如果沒(méi)有出現(xiàn)零我們可以返回結(jié)果的 Just。下面是這個(gè)算法的實(shí)現(xiàn):
-- file: ch19/divby2.hs
divBy :: Integral a => a -> [a] -> Maybe [a]
divBy _ [] = Just []
divBy _ (0:_) = Nothing
divBy numerator (denom:xs) =
case divBy numerator xs of
Nothing -> Nothing
Just results -> Just ((numerator `div` denom) : results)
如果你在 ghci 中嘗試它,你會(huì)發(fā)現(xiàn)它可以工作:
ghci> divBy 50 [1,2,5,8,10]
Just [50,25,10,6,5]
ghci> divBy 50 [1,2,0,8,10]
Nothing
調(diào)用 divBy 的函數(shù)現(xiàn)在可以使用 case 語(yǔ)句來(lái)觀察調(diào)用成功與否,就像 divBy 調(diào)用自己時(shí)所做的那樣。
Tip
你大概注意到,上面可以使用一個(gè)monadic的實(shí)現(xiàn),像這樣子:
-- file: ch19/divby2m.hs
divBy :: Integral a => a -> [a] -> Maybe [a]
divBy numerator denominators =
mapM (numerator `safeDiv`) denominators
where safeDiv _ 0 = Nothing
safeDiv x y = x `div` y
出于簡(jiǎn)單考慮,在這章中我們會(huì)避免使用monadic實(shí)現(xiàn),但是會(huì)指出有這種做法。
[譯注:Tip中那段代碼編譯不過(guò)]
使用 Maybe 很方便,但是有代價(jià)。 divBy 將不能夠再處理無(wú)限的鏈表輸入。由于結(jié)果是一個(gè) Maybe[a] ,必須要檢查整個(gè)輸入鏈表,我們才能確認(rèn)不會(huì)因?yàn)榇嬖诹愣祷?Nothing 。你可以嘗試在之前的例子中驗(yàn)證這一點(diǎn):
ghci> divBy 100 [1..]
*** Exception: stack overflow
這里觀察到,你沒(méi)有看到部分的輸出;你沒(méi)得到任何輸出。注意到在 divBy 的每一步中(除了輸入鏈表為空或者鏈表開(kāi)頭是零的情況),每個(gè)子序列元素的結(jié)果必須先于當(dāng)前元素的結(jié)果得到。因此這個(gè)算法無(wú)法處理無(wú)窮鏈表,并且對(duì)于大的有限鏈表,它的空間效率也不高。
之前已經(jīng)說(shuō)過(guò), Maybe 通常是一個(gè)好的選擇。在這個(gè)特殊例子中,只有當(dāng)我們?nèi)?zhí)行整個(gè)輸入的時(shí)候我們才知道是否有問(wèn)題。有時(shí)候我們可以提交發(fā)現(xiàn)問(wèn)題,例如,在 ghci 中 tail[] 會(huì)生成一個(gè)異常。我們可以很容易寫(xiě)一個(gè)可以處理無(wú)窮情況的 tail :
-- file: ch19/safetail.hs
safeTail :: [a] -> Maybe [a]
safeTail [] = Nothing
safeTail (_:xs) = Just xs
如果輸入為空,簡(jiǎn)單的返回一個(gè) Nothing ,其它情況返回結(jié)果的 Just 。由于在知道是否發(fā)生錯(cuò)誤之前,我們只需要確認(rèn)鏈表非空,在這里使用 Maybe 不會(huì)破壞惰性。我們可以在 ghci 中測(cè)試并觀察跟普通的 tail 有何不同:
ghci> tail [1,2,3,4,5]
[2,3,4,5]
ghci> safeTail [1,2,3,4,5]
Just [2,3,4,5]
ghci> tail []
*** Exception: Prelude.tail: empty list
ghci> safeTail []
Nothing
這里我們可以看到,我們的 safeTail 執(zhí)行結(jié)果符合預(yù)期。但是對(duì)于無(wú)窮鏈表呢?我們不想打印無(wú)窮的結(jié)果的數(shù)字,所以我們用 take5(tail[1..]) 以及一個(gè)類(lèi)似的saftTail構(gòu)建測(cè)試:
ghci> take 5 (tail [1..])
[2,3,4,5,6]
ghci> case safeTail [1..] of {Nothing -> Nothing; Just x -> Just (take 5 x)}
Just [2,3,4,5,6]
ghci> take 5 (tail [])
*** Exception: Prelude.tail: empty list
ghci> case safeTail [] of {Nothing -> Nothing; Just x -> Just (take 5 x)}
Nothing
這里你可以看到 tail 和 safeTail 都可以處理無(wú)窮鏈表。注意我們可以更好地處理空的輸入鏈表;而不是拋出異常,我們決定這種情況返回 Nothing 。我們可以獲得錯(cuò)誤處理能力卻不會(huì)失去惰性。
但是我們?nèi)绾螌⑺鼞?yīng)用到我們的 divBy 的例子中呢?讓我們思考下現(xiàn)在的情況:失敗是單個(gè)壞的輸入的屬性,而不是輸入鏈表自身。那么將失敗作為單個(gè)輸出元素的屬性,而不是整個(gè)輸出鏈表怎么樣?也就是說(shuō),不是一個(gè)類(lèi)型為 a->[a]->Maybe[a] 的函數(shù),取而代之我們使用 a->[a]->[Maybea] 。這樣做的好處是可以保留惰性,并且調(diào)用者可以確定是在鏈表中的哪里出了問(wèn)題–或者甚至是過(guò)濾掉有問(wèn)題的結(jié)果,如果需要的話。這里是一個(gè)實(shí)現(xiàn):
-- file: ch19/divby3.hs
divBy :: Integral a => a -> [a] -> [Maybe a]
divBy numerator denominators =
map worker denominators
where worker 0 = Nothing
worker x = Just (numerator `div` x)
看下這個(gè)函數(shù),我們?cè)俅位氐绞褂?map ,這無(wú)論對(duì)簡(jiǎn)潔和惰性都是件好事。我們可以在 ghci 中測(cè)試它,并觀察對(duì)于有限和無(wú)限鏈表它都可以正常工作:
ghci> divBy 50 [1,2,5,8,10]
[Just 50,Just 25,Just 10,Just 6,Just 5]
ghci> divBy 50 [1,2,0,8,10]
[Just 50,Just 25,Nothing,Just 6,Just 5]
ghci> take 5 (divBy 100 [1..])
[Just 100,Just 50,Just 33,Just 25,Just 20]
我們希望通過(guò)這個(gè)討論你可以明白這點(diǎn),不符合規(guī)范的(正如 safeTail 中的情況)輸入和包含壞的數(shù)據(jù)的輸入( divBy 中的情況)是有區(qū)別的。這兩種情況通常需要對(duì)結(jié)果采用不同的處理。
回到 使用Maybe 這一節(jié),我們有一個(gè)叫做 divby2.hs 的示例程序。這個(gè)例子沒(méi)有保存惰性,而是返回一個(gè)類(lèi)型為 Maybe[a] 的值。用monadic風(fēng)格也可以表達(dá)同樣的算法。更多信息和monad相關(guān)背景,參考 第14章Monads [http://rwh.readthedocs.org/en/latest/chp/14.html] 。這是我們新的monadic風(fēng)格的算法:
-- file: ch19/divby4.hs
divBy :: Integral a => a -> [a] -> Maybe [a]
divBy _ [] = return []
divBy _ (0:_) = fail "division by zero in divBy"
divBy numerator (denom:xs) =
do next <- divBy numerator xs
return ((numerator `div` denom) : next)
Maybe monad使得這個(gè)算法的表示看上去更好。對(duì)于 Maybe monad, return 就跟 Just 一樣,并且 fail_=Nothing ,因此我們看到任何的錯(cuò)誤說(shuō)明的字段串。我們可以用我們?cè)?divby2.hs 中使用過(guò)的測(cè)試來(lái)測(cè)試這個(gè)算法:
ghci> divBy 50 [1,2,5,8,10]
Just [50,25,10,6,5]
ghci> divBy 50 [1,2,0,8,10]
Nothing
ghci> divBy 100 [1..]
*** Exception: stack overflow
我們寫(xiě)的代碼實(shí)際上并不限于 Maybe monad。只要簡(jiǎn)單地改變類(lèi)型,我們可以讓它對(duì)于任何monad都能工作。讓我們?cè)囈幌拢?/p>
-- file: ch19/divby5.hs
divBy :: Integral a => a -> [a] -> Maybe [a]
divBy = divByGeneric
divByGeneric :: (Monad m, Integral a) => a -> [a] -> m [a]
divByGeneric _ [] = return []
divByGeneric _ (0:_) = fail "division by zero in divByGeneric"
divByGeneric numerator (denom:xs) =
do next <- divByGeneric numerator xs
return ((numerator `div` denom) : next)
函數(shù) divByGeneric 包含的代碼 divBy 之前所做的一樣;我們只是給它一個(gè)更通用的類(lèi)型。事實(shí)上,如果不給出類(lèi)型,這個(gè)類(lèi)型是由 ghci 自動(dòng)推導(dǎo)的。我們還為特定的類(lèi)型定義了一個(gè)更方便的函數(shù) divBy 。
讓我們?cè)?ghci 中運(yùn)行一下。
ghci> :l divby5.hs
[1 of 1] Compiling Main ( divby5.hs, interpreted )
Ok, modules loaded: Main.
ghci> divBy 50 [1,2,5,8,10]
Just [50,25,10,6,5]
ghci> (divByGeneric 50 [1,2,5,8,10])::(Integral a => Maybe [a])
Just [50,25,10,6,5]
ghci> divByGeneric 50 [1,2,5,8,10]
[50,25,10,6,5]
ghci> divByGeneric 50 [1,2,0,8,10]
*** Exception: user error (division by zero in divByGeneric)
前兩個(gè)例子產(chǎn)生的輸出都跟我們之前看到的一樣。由于 divByGeneric 沒(méi)有指定返回的類(lèi)型,我們要么指定一個(gè),要么讓解釋器從環(huán)境中推導(dǎo)得到。如果我們不指定返回類(lèi)型, ghic 推薦得到 IO monad。在第三和第四個(gè)例子中你可以看出來(lái)。在第四個(gè)例子中你可以看到, IO monad將 fail 轉(zhuǎn)化成了一個(gè)異常。
mtl 包中的 Control.Monad.Error 模塊也將 EitherString 變成了一個(gè)monad。如果你使用 Either ,你可以得到保存了錯(cuò)誤信息的純的結(jié)果,像這樣子:
ghci> :m +Control.Monad.Error
ghci> (divByGeneric 50 [1,2,5,8,10])::(Integral a => Either String [a])
Loading package mtl-1.1.0.0 ... linking ... done.
Right [50,25,10,6,5]
ghci> (divByGeneric 50 [1,2,0,8,10])::(Integral a => Either String [a])
Left "division by zero in divByGeneric"
這讓我們進(jìn)入到下一個(gè)話題的討論:使用 Either 返回錯(cuò)誤信息。
Either 類(lèi)型跟 Maybe 類(lèi)型類(lèi)似,除了一處關(guān)鍵的不同:對(duì)于錯(cuò)誤或者成功(“ Right 類(lèi)型”),它都可以攜帶數(shù)據(jù)。盡管語(yǔ)言沒(méi)有強(qiáng)加任何限制,按照慣例,一個(gè)返回 Either 的函數(shù)使用 Left 返回值來(lái)表示一個(gè)錯(cuò)誤, Right 來(lái)表示成功。如果你覺(jué)得這樣有助于記憶,你可以認(rèn)為 Right 表式正確結(jié)果。我們可以改一下前面小節(jié)中關(guān)于 Maybe 時(shí)使用的 divby2.hs 的例子,讓 Either 可以工作:
-- file: ch19/divby6.hs
divBy :: Integral a => a -> [a] -> Either String [a]
divBy _ [] = Right []
divBy _ (0:_) = Left "divBy: division by 0"
divBy numerator (denom:xs) =
case divBy numerator xs of
Left x -> Left x
Right results -> Right ((numerator `div` denom) : results)
這份代碼跟 Maybe 的代碼幾乎是完全一樣的;我們只是把每個(gè) Just 用 Right 替換。Left 對(duì)應(yīng)于 Nothing ,但是現(xiàn)在它可以攜帶一條信息。讓我們?cè)?ghci 里面運(yùn)行一下:
ghci> divBy 50 [1,2,5,8,10]Right [50,25,10,6,5]ghci> divBy 50 [1,2,0,8,10]Left “divBy: division by 0”
盡管用 String 類(lèi)型來(lái)表示錯(cuò)誤的原因?qū)窈蠛苡泻锰?,自定義的錯(cuò)誤類(lèi)型通常會(huì)更有幫助。使用自定義的錯(cuò)誤類(lèi)型我們可以知道到底是出了什么問(wèn)題,并且獲知是什么動(dòng)作引發(fā)的這個(gè)問(wèn)題。例如,讓我們假設(shè),由于某些原因,不僅僅是除0,我們還不想除以10或者20。我們可以像這樣子自定義一個(gè)錯(cuò)誤類(lèi)型:
-- file: ch19/divby7.hs
data DivByError a = DivBy0
| ForbiddenDenominator a
deriving (Eq, Read, Show)
divBy :: Integral a => a -> [a] -> Either (DivByError a) [a]
divBy _ [] = Right []
divBy _ (0:_) = Left DivBy0
divBy _ (10:_) = Left (ForbiddenDenominator 10)
divBy _ (20:_) = Left (ForbiddenDenominator 20)
divBy numerator (denom:xs) =
case divBy numerator xs of
Left x -> Left x
Right results -> Right ((numerator `div` denom) : results)
現(xiàn)在,在出現(xiàn)錯(cuò)誤時(shí),可以通過(guò) Left 數(shù)據(jù)檢查導(dǎo)致錯(cuò)誤的準(zhǔn)確原因。或者,可以簡(jiǎn)單的只是通過(guò) show 打印出來(lái)。下面是這個(gè)函數(shù)的應(yīng)用:
ghci> divBy 50 [1,2,5,8]
Right [50,25,10,6]
ghci> divBy 50 [1,2,5,8,10]
Left (ForbiddenDenominator 10)
ghci> divBy 50 [1,2,0,8,10]
Left DivBy0
Warning
所有這些 Either 的例子都跟我們之前的 Maybe 一樣,都會(huì)遇到失去惰性的問(wèn)題。我們將在這一章的最后用一個(gè)練習(xí)題來(lái)解決這個(gè)問(wèn)題。
回到 Maybe Monad的用法 這一節(jié),我們向你展示了如何在一個(gè)monad中使用 Maybe 。 Either 也可以在monad中使用,但是可能會(huì)復(fù)雜一點(diǎn)。原因是 fail 是硬編碼的只接受 String 作為失敗代碼,因此我們必須有一種方法將這樣的字符串映射成我們的 Left 使用的類(lèi)型。正如你前面所見(jiàn), Control.Monad.Error 為 EitherStringa 提供了內(nèi)置的支持,它沒(méi)有涉及到將參數(shù)映射到 fail 。這里我們可以將我們的例子修改為monadic風(fēng)格使得 Either 可以工作:
-- file: ch19/divby8.hs
{-# LANGUAGE FlexibleContexts #-}
import Control.Monad.Error
data Show a =>
DivByError a = DivBy0
| ForbiddenDenominator a
| OtherDivByError String
deriving (Eq, Read, Show)
instance Error (DivByError a) where
strMsg x = OtherDivByError x
divBy :: Integral a => a -> [a] -> Either (DivByError a) [a]
divBy = divByGeneric
divByGeneric :: (Integral a, MonadError (DivByError a) m) =>
a -> [a] -> m [a]
divByGeneric _ [] = return []
divByGeneric _ (0:_) = throwError DivBy0
divByGeneric _ (10:_) = throwError (ForbiddenDenominator 10)
divByGeneric _ (20:_) = throwError (ForbiddenDenominator 20)
divByGeneric numerator (denom:xs) =
do next <- divByGeneric numerator xs
return ((numerator `div` denom) : next)
這里,我們需要打開(kāi) FlexibleContexts 語(yǔ)言擴(kuò)展以提供 divByGeneric 的類(lèi)型簽名。 divBy 函數(shù)跟之前的工作方式完全一致。對(duì)于 divByGeneric ,我們將 divByError 做為 Error 類(lèi)型類(lèi)的成員,通過(guò)定義調(diào)用 fail 時(shí)的行為( strMsg 函數(shù))。我們還將 Right 轉(zhuǎn)化成 return ,將 Left 轉(zhuǎn)化成 throwError 進(jìn)行泛化。
許多語(yǔ)言中都有異常處理,包括Haskell。異常很有用,因?yàn)楫?dāng)發(fā)生故障時(shí),它提供了一種簡(jiǎn)單的處理方法,即使故障離發(fā)生的地方沿著函數(shù)調(diào)用鏈走了幾層。有了異常,不需要檢查每個(gè)函數(shù)調(diào)用的返回值是否發(fā)生了錯(cuò)誤,不需要注意去生成表示錯(cuò)誤的返回值,像C程序員必須這么做。在Haskell中,由于有 monad以及 Either 和 Maybe 類(lèi)型,你通常可以在純的代碼中達(dá)到同樣的效果而不需要使用異常和異常處理。
有些問(wèn)題–尤其是涉及到IO調(diào)用–需要處理異常。在Haskell中,異??赡軙?huì)在程序的任何地方拋出。然而,由于計(jì)算順序是不確定的,異常只可以在 IO monad中捕獲。Haskell異常處理不涉及像Python或者Java中那樣的特殊語(yǔ)法。捕獲和處理異常的技術(shù)是–真令人驚訝–函數(shù)。
在 Control.Exception 模塊中,定義了各種跟異常相關(guān)的函數(shù)和類(lèi)型。 Exception 類(lèi)型是在那里定義的;所有的異常的類(lèi)型都是 Exception 。還有用于捕獲和處理異常的函數(shù)。讓我們先看一看 try ,它的類(lèi)型是 IOa->IO(EitherExceptiona) 。它將異常處理包裝在 IO 中。如果有異常拋出,它會(huì)返回一個(gè) Left 值表示異常;否則,返回原始結(jié)果到 Right 值。讓我們?cè)?ghci 中運(yùn)行一下。我們首先觸發(fā)一個(gè)未處理的異常,然后嘗試捕獲它。
ghci> :m Control.Exception
ghci> let x = 5 `div` 0
ghci> let y = 5 `div` 1
ghci> print x
*** Exception: divide by zero
ghci> print y
5
ghci> try (print x)
Left divide by zero
ghci> try (print y)
5
Right ()
注意到在 let 語(yǔ)句中沒(méi)有拋出異常。這是意料之中的,是因?yàn)槎栊郧笾?;除以零只有到打?x 的值的時(shí)候才需要計(jì)算。還有,注意 try(printy) 有兩行輸出。第一行是由 print 產(chǎn)生的,它在終端上顯示5。第二個(gè)是由 ghci 生成的,這個(gè)表示 printy 的返回值為 () 并且沒(méi)有拋出異常。
既然你知道了 try 是如何工作的,讓我們?cè)囅铝硪粋€(gè)實(shí)驗(yàn)。讓我們假設(shè)我們想捕獲 try 的結(jié)果用于后續(xù)的計(jì)算,這樣我們可以處理除的結(jié)果。我們大概會(huì)這么做:
ghci> result <- try (return x)
Right *** Exception: divide by zero
這里發(fā)生了什么?讓我們拆成一步一步看,先試下另一個(gè)例子:
ghci> let z = undefined
ghci> try (print z)
Left Prelude.undefined
ghci> result <- try (return z)
Right *** Exception: Prelude.undefined
跟之前一樣,將 undefined 賦值給 z 沒(méi)什么問(wèn)題。問(wèn)題的關(guān)鍵,以及前面的迷惑,都在于惰性求值。準(zhǔn)確地說(shuō),是在于 return ,它沒(méi)有強(qiáng)制它的參數(shù)的執(zhí)行;它只是將它包裝了一下。這樣, try(returnundefined) 的結(jié)果應(yīng)該是 Rightundefined ?,F(xiàn)在, ghci 想要將這個(gè)結(jié)果顯示在終端上。它將運(yùn)行到打印”Right”,但是 undefined 無(wú)法打?。ɑ蛘哒f(shuō)除以零的結(jié)果無(wú)法打?。R虼四憧吹搅水惓P畔?,它是來(lái)源于 ghci 的,而不是你的程序。
這是一個(gè)關(guān)鍵點(diǎn)。讓我們想想為什么之前的例子可以工作,而這個(gè)不可以。之前,我們把 printx 放在了 try 里面。打印一些東西的值,固然是需要執(zhí)行它的,因此,異常在正確的地方被檢測(cè)到了。但是,僅僅是使用 return 并不會(huì)強(qiáng)制計(jì)算的執(zhí)行。為了解決這個(gè)問(wèn)題, Control.Exception 模塊中定義了一個(gè) evaluate 函數(shù)。它的行為跟 return 類(lèi)似,但是會(huì)讓參數(shù)立即執(zhí)行。讓我們?cè)囈幌拢?/p>
ghci> let z = undefined
ghci> result <- try (evaluate z)
Left Prelude.undefined
ghci> result <- try (evaluate x)
Left divide by zero
看,這就是我們想要的答案。無(wú)論對(duì)于 undefiined 還是除以零的例子,都可以正常工作。
Tip
記?。喝魏螘r(shí)候你想捕獲純的代碼中拋出的異常,在你的異常處理函數(shù)中使用 evaluate 而不是 return 。
通常,你可能希望如果一塊代碼中沒(méi)有任何異常發(fā)生,就執(zhí)行某個(gè)動(dòng)作,否則執(zhí)行不同的動(dòng)作。對(duì)于像這種場(chǎng)合,有一個(gè)叫做 handle 的函數(shù)。這個(gè)函數(shù)的類(lèi)型是 (Exception->IOa)->IOa->IOa 。即是說(shuō),它需要兩個(gè)參數(shù):前一個(gè)是一個(gè)函數(shù),當(dāng)執(zhí)行后一個(gè)動(dòng)作發(fā)生異常的時(shí)候它會(huì)被調(diào)用。下面是我們使用的一種方式:
ghci> :m Control.Exception
ghci> let x = 5 `div` 0
ghci> let y = 5 `div` 1
ghci> handle (\_ -> putStrLn "Error calculating result") (print x)
Error calculating result
ghci> handle (\_ -> putStrLn "Error calculating result") (print y)
5
像這樣,如果計(jì)算中沒(méi)有錯(cuò)誤發(fā)生,我們可以打印一條好的信息。這當(dāng)然要比除以零出錯(cuò)時(shí)程序崩潰要好。
上面的例子的一個(gè)問(wèn)題是,對(duì)于任何異常它都是打印 “Error calculating result”??赡軙?huì)有些其它不是除零的異常。例如,顯示輸出時(shí)可能會(huì)發(fā)生錯(cuò)誤,或者純的代碼中可能拋出一些其它的異常。
handleJust 函數(shù)就是處理這種情況的。它讓你指定一個(gè)測(cè)試來(lái)決定是否對(duì)給定的異常感興趣。讓我們看一下:
-- file: ch19/hj1.hs
import Control.Exception
catchIt :: Exception -> Maybe ()
catchIt (ArithException DivideByZero) = Just ()
catchIt _ = Nothing
handler :: () -> IO ()
handler _ = putStrLn "Caught error: divide by zero"
safePrint :: Integer -> IO ()
safePrint x = handleJust catchIt handler (print x)
cacheIt 定義了一個(gè)函數(shù),這個(gè)函數(shù)會(huì)決定我們對(duì)給定的異常是否感興趣。如果是,它會(huì)返回 Just ,否則返回 Nothing 。還有, Just 中附帶的值會(huì)被傳到我們的處理函數(shù)中?,F(xiàn)在我們可以很好地使用 safePrint 了:
ghci> :l hj1.hs[1 of 1] Compiling Main ( hj1.hs, interpreted )Ok, modules loaded: Main.ghci> let x = 5 div 0ghci> let y = 5 div 1ghci> safePrint xCaught error: divide by zeroghci> safePrint y5
Control.Exception 模塊還提供了一些可以在 handleJust 中使用的函數(shù),以便于我們將異常的范圍縮小到我們所關(guān)心的類(lèi)別。例如,有個(gè)函數(shù) arithExceptions 類(lèi)型是 Exception->MaybeArithException 可以挑選出任意的 ArithException 異常,但是會(huì)忽略掉其它。我們可以像這樣使用它:
-- file: ch19/hj2.hs
import Control.Exception
handler :: ArithException -> IO ()
handler e = putStrLn $ "Caught arithmetic error: " ++ show e
safePrint :: Integer -> IO ()
safePrint x = handleJust arithExceptions handler (print x)
用這種方式,我們可以捕獲所有 ArithException 類(lèi)型的異常,但是仍然讓其它的異常通過(guò),不捕獲也不修改。我們可以看到它是這樣工作的:
ghci> :l hj2.hs
[1 of 1] Compiling Main ( hj2.hs, interpreted )
Ok, modules loaded: Main.
ghci> let x = 5 `div` 0
ghci> let y = 5 `div` 1
ghci> safePrint x
Caught arithmetic error: divide by zero
ghci> safePrint y
5
其中特別感興趣的是,你大概注意到了 ioErrors 測(cè)試,這是跟一大類(lèi)的I/O相關(guān)的異常。
大概在任何程序中異常最大的來(lái)源就是I/O。在處理外部世界的時(shí)候所有事情都可能出錯(cuò):磁盤(pán)滿了,網(wǎng)絡(luò)斷了,或者你期望文件里面有數(shù)據(jù)而文件卻是空的。在Haskell中,I/O異常就跟其它的異常一樣可以用 Exception 數(shù)據(jù)類(lèi)型來(lái)表示。另一方面,由于有這么多類(lèi)型的I/O異常,有一個(gè)特殊的模塊– System.IO.Error 專(zhuān)門(mén)用于處理它們。
System.IO.Error 定義了兩個(gè)函數(shù): catch 和 try ,跟 Control.Exception 中的類(lèi)似,它們都是用于處理異常的。然而,不像 Control.Exception 中的函數(shù),這些函數(shù)只會(huì)捕獲I/O錯(cuò)誤,而不處理其它類(lèi)型異常。在Haskell中,所有I/O錯(cuò)誤有一個(gè)共同類(lèi)型 IOError ,它的定義跟 IOException 是一樣的。
Tip
當(dāng)心你使用的哪個(gè)名字因?yàn)?System.IO.Error 和 Control.Exception 定義了同樣名字的函數(shù),如果你將它們都導(dǎo)入你的程序,你將收到一個(gè)錯(cuò)誤信息說(shuō)引用的函數(shù)有歧義。你可以通過(guò) qualified 引用其中一個(gè)或者另一個(gè),或者將其中一個(gè)或者另一個(gè)的符號(hào)隱藏。
注意 Prelude 導(dǎo)出的是 System.IO.Error 中的 catch ,而不是 ControlException 中提供的。記住,前者只捕獲I/O錯(cuò)誤,而后者捕獲所有的異常。換句話說(shuō), 你要的幾乎總是 Control.Exception 中的那個(gè) catch ,而不是默認(rèn)的那個(gè)。
讓我們看一下對(duì)我們有益的一個(gè)在I/O系統(tǒng)中使用異常的方法。在 使用文件和句柄 [http://rwh.readthedocs.org/en/latest/chp/7.html#handle] 這一節(jié)里,我們展示了一個(gè)使用命令式風(fēng)格從文件中一行一行的讀取的程序。盡管我們后面也示范過(guò)更簡(jiǎn)潔的,更”Haskelly”的方式解決那個(gè)問(wèn)題,讓我們?cè)谶@里重新審視這個(gè)例子。在 mainloop 函數(shù)中,在讀一行之前,我們必須明確地測(cè)試我們的輸入文件是否結(jié)束。這次,我們可以檢查嘗試讀一行是否會(huì)導(dǎo)致一個(gè)EOF錯(cuò)誤,像這樣子:
-- file: ch19/toupper-impch20.hs
import System.IO
import System.IO.Error
import Data.Char(toUpper)
main :: IO ()
main = do
inh <- openFile "input.txt" ReadMode
outh <- openFile "output.txt" WriteMode
mainloop inh outh
hClose inh
hClose outh
mainloop :: Handle -> Handle -> IO ()
mainloop inh outh =
do input <- try (hGetLine inh)
case input of
Left e ->
if isEOFError e
then return ()
else ioError e
Right inpStr ->
do hPutStrLn outh (map toUpper inpStr)
mainloop inh outh
這里,我們使用 System.IO.Error 中的 try 來(lái)檢測(cè)是否 hGetLine 拋出一個(gè) IOError 。如果是,我們使用 isEOFError (在 System.IO.Error 中定義)來(lái)看是否拋出異常表明我們到達(dá)了文件末尾。如果是的,我們退出循環(huán)。如果是其它的異常,我們調(diào)用 ioError 重新拋出它。
有許多的這種測(cè)試和方法可以從 System.IO.Error 中定義的 IOError 中提取信息。我們推薦你在需要的時(shí)候去查一下庫(kù)的參考頁(yè)。
到現(xiàn)在為止,我們已經(jīng)詳細(xì)地討論了異常處理。還有另外一個(gè)困惑:拋出異常。到目前為止這一章我們所接觸到的例子中,都是由Haskell為你拋出異常的。然后你也可以自己拋出任何異常。我們會(huì)告訴你怎么做。
你將會(huì)注意到這些函數(shù)大部分似乎返回一個(gè)類(lèi)型為 a 或者 IOa 的值。這意味著這個(gè)函數(shù)似乎可以返回任意類(lèi)型的值。事實(shí)上,由于這些函數(shù)會(huì)拋出異常,一般情況下它們決不“返回”任何東西。這些返回值讓你可以在各種各樣的上下文中使用這些函數(shù),不同的上下文需要不同的類(lèi)型。
讓我們使用函數(shù) Control.Exception 來(lái)開(kāi)始我們的拋出異常的教程。最通用的函數(shù)是 throw ,它的類(lèi)型是 Exception->a 。這個(gè)函數(shù)可以拋出任何的 Exception ,并且可以用于純的上下文中。還有一個(gè)類(lèi)型為 Exception->IOa 的函數(shù) throwIO 在 IO monad中拋出異常。這兩個(gè)函數(shù)都需要一個(gè) Exception 用于拋出。你可以手工制作一個(gè) Exception ,或者重用之前創(chuàng)建的 Exception 。
還有一個(gè)函數(shù) ioError ,它在 Control.Exception 和 System.IO.Error 中定義都是相同的,它的類(lèi)型是 IOError->IOa 。當(dāng)你想生成任意的I/O相關(guān)的異常的時(shí)候可以使用它。
這需要使用兩個(gè)很不常用的Haskell模塊: Data.Dynamic 和 Data.Typeable 。我們不會(huì)講太多關(guān)于這些模塊,但是告訴你當(dāng)你需要制作自己的動(dòng)態(tài)異常類(lèi)型時(shí),可以使用這些工具。
在 第二十一章使用數(shù)據(jù)庫(kù)http://book.realworldhaskell.org/read/using-databases.html 中,你會(huì)看到HDBC數(shù)據(jù)庫(kù)庫(kù)使用動(dòng)態(tài)異常來(lái)表示SQL數(shù)據(jù)庫(kù)返回給應(yīng)用的錯(cuò)誤。數(shù)據(jù)庫(kù)引擎返回的錯(cuò)誤通常有三個(gè)組件:一個(gè)表示錯(cuò)誤碼的整數(shù),一個(gè)狀態(tài),以及一條人類(lèi)可讀的錯(cuò)誤消息。在這一章中我們會(huì)創(chuàng)建我們自己的HDBC SqlError 實(shí)現(xiàn)。讓我們從錯(cuò)誤自身的數(shù)據(jù)結(jié)構(gòu)表示開(kāi)始:
-- file: ch19/dynexc.hs
{-# LANGUAGE DeriveDataTypeable #-}
import Data.Dynamic
import Control.Exception
data SqlError = SqlError {seState :: String,
seNativeError :: Int,
seErrorMsg :: String}
deriving (Eq, Show, Read, Typeable)
通過(guò)繼承 Typeable 類(lèi)型類(lèi),我們使這個(gè)類(lèi)型可用于動(dòng)態(tài)的類(lèi)型編程。為了讓GHC自動(dòng)生成一個(gè) Typeable 實(shí)例,我們要開(kāi)啟 DeriveDataTypeable 語(yǔ)言擴(kuò)展。
現(xiàn)在,讓我們定義一個(gè) catchSql 和一個(gè) handleSql 用于捕獵一個(gè) SqlError 異常。注意常規(guī)的 catch 和 handle 函數(shù)無(wú)法捕獵我們的 SqlError ,因?yàn)樗皇?Exception 類(lèi)型的。
-- file: ch19/dynexc.hs
{- | Execute the given IO action.
If it raises a 'SqlError', then execute the supplied
handler and return its return value. Otherwise, proceed
as normal. -}
catchSql :: IO a -> (SqlError -> IO a) -> IO a
catchSql = catchDyn
{- | Like 'catchSql', with the order of arguments reversed. -}
handleSql :: (SqlError -> IO a) -> IO a -> IO a
handleSql = flip catchSql
[譯注:原文中文件名是dynexc.hs,但是跟前面的沖突了,所以這里重命名為dynexc1.hs]
這些函數(shù)僅僅是在 catchDyn 外面包了很薄的一層,類(lèi)型是 Typeableexception=>IOa->(exception->IOa)->IOa 。這里我們簡(jiǎn)單地限定了它的類(lèi)型使得它只捕獵SQL異常。
正常地,當(dāng)一個(gè)異常拋出,但是沒(méi)有在任何地方被捕獲,程序會(huì)崩潰并顯示異常到標(biāo)準(zhǔn)錯(cuò)誤輸出。然而,對(duì)于動(dòng)態(tài)異常,系統(tǒng)不會(huì)知道該如何顯示它,因此你將僅僅會(huì)看到一個(gè)的”unknown exception”消息,這可能沒(méi)太大幫助。我們可以提供一個(gè)輔助函數(shù),這樣應(yīng)用可以寫(xiě)成,比如說(shuō) main=handleSqlError$do... ,使拋出的異??梢燥@示。下面是如何寫(xiě) handleSqlError :
-- file: ch19/dynexc.hs
{- | Catches 'SqlError's, and re-raises them as IO errors with fail.
Useful if you don't care to catch SQL errors, but want to see a sane
error message if one happens. One would often use this as a
high-level wrapper around SQL calls. -}
handleSqlError :: IO a -> IO a
handleSqlError action =
catchSql action handler
where handler e = fail ("SQL error: " ++ show e)
[譯注:原文中是dynexc.hs,這里重命名過(guò)文件]
最后,讓我們給出一個(gè)如何拋出 SqlError 異常的例子。下面的函數(shù)做的就是這件事:
-- file: ch19/dynexc.hs
throwSqlError :: String -> Int -> String -> a
throwSqlError state nativeerror errormsg =
throwDyn (SqlError state nativeerror errormsg)
throwSqlErrorIO :: String -> Int -> String -> IO a
throwSqlErrorIO state nativeerror errormsg =
evaluate (throwSqlError state nativeerror errormsg)
Tip
提醒一下, evaluate 跟 return 類(lèi)似但是會(huì)立即計(jì)算它的參數(shù)。
這樣我們的動(dòng)態(tài)異常的支持就完成了。代碼很多,你大概不需要這么多代碼,但是我們想要給你一個(gè)動(dòng)態(tài)異常自身的例子以及和它相關(guān)的工具。事實(shí)上,這里的例子幾乎就反映在HDBC庫(kù)中。讓我們?cè)?ghci 中試一下:
ghci> :l dynexc.hs
[1 of 1] Compiling Main ( dynexc.hs, interpreted )
Ok, modules loaded: Main.
ghci> throwSqlErrorIO "state" 5 "error message"
*** Exception: (unknown)
ghci> handleSqlError $ throwSqlErrorIO "state" 5 "error message"
*** Exception: user error (SQL error: SqlError {seState = "state", seNativeError = 5, seErrorMsg = "error message"})
ghci> handleSqlError $ fail "other error"
*** Exception: user error (other error)
這里你可以看出, ghci 自己并不知道如何顯示SQL錯(cuò)誤。但是,你可以看到 handleSqlError 幫助做了這些,不過(guò)沒(méi)有捕獲其它的錯(cuò)誤。最后讓我們?cè)囈粋€(gè)自定義的handler:
ghci> handleSql (fail . seErrorMsg) (throwSqlErrorIO "state" 5 "my error")
*** Exception: user error (my error)
這里,我們自定義了一個(gè)錯(cuò)誤處理拋出一個(gè)新的異常,構(gòu)成 SqlError 中的 seErrorMsg 域。你可以看到它是按預(yù)想中那樣工作的。
因?yàn)槲覀儽仨毑东@ IO monad中的異常,如果我們?cè)谝粋€(gè)monad中或者在monad的轉(zhuǎn)化棧中使用它們,我們將跳出到 IO monad。這幾乎肯定不是我們想要的。
在 構(gòu)建以理解Monad變換器 [http://rwh.readthedocs.org/en/latest/chp/18.html#id9] 中我們定義了一個(gè) MaybeT 的變換,但是它更像是一個(gè)有助于理解的東西,而不是編程的工具。幸運(yùn)的是,已經(jīng)有一個(gè)專(zhuān)門(mén)的–也更有用的–monad變換: ErrorT ,它是定義在 Control.Monad.Error 模塊中的。
ErrorT 變換器使我們可以向monad中添加異常,但是它使用了特殊的方法,跟 Control.Exception 模塊中提供的不一樣。它提供給我們一些有趣的能力。
Warning
不要把ErrorT跟普通異?;煜绻覀?cè)?ErrorT 內(nèi)面使用 Control.Exception 中的 throw 函數(shù),我們?nèi)匀粫?huì)彈出到 IO monad。
正如其它的 mtl monad一樣, ErrorT 提供的接口是由一個(gè)類(lèi)型類(lèi)定義的。
-- file: ch19/MonadError.hs
class (Monad m) => MonadError e m | m -> e where
throwError :: e -- error to throw
-> m a
catchError :: m a -- action to execute
-> (e -> m a) -- error handler
-> m a
類(lèi)型變量 e 代表我們想要使用的錯(cuò)誤類(lèi)型。不管我們的錯(cuò)誤類(lèi)型是什么,我們必須將它做成 Error 類(lèi)型類(lèi)的實(shí)例。
-- file: ch19/MonadError.hs
class Error a where
-- create an exception with no message
noMsg :: a
-- create an exception with a message
strMsg :: String -> a
ErrorT 實(shí)現(xiàn) fail 時(shí)會(huì)用到 strMsg 函數(shù)。它將 strMsg 作為一個(gè)異常拋出,將自己接收到的字符串參數(shù)傳遞給這個(gè)異常。對(duì)于 noMsg ,它是用于提供 MonadPlus 類(lèi)型類(lèi)中的 mzero 的實(shí)現(xiàn)。
為了支持 strMsg 和 noMsg 函數(shù),我們的 ParseError 類(lèi)型會(huì)有一個(gè) Chatty 構(gòu)造器。這個(gè)將用作構(gòu)造器如果,比如說(shuō),有人在我們的monad中調(diào)用 fail 。
我們需要知道的最后一塊是關(guān)于執(zhí)行函數(shù) runErrorT 的類(lèi)型。
ghci> :t runErrorT
runErrorT :: ErrorT e m a -> m (Either e a)
為了說(shuō)明 ErrorT 的使用,讓我們開(kāi)發(fā)一個(gè)類(lèi)似于Parsec的解析庫(kù)的基本的骨架。
-- file: ch19/ParseInt.hs
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
import Control.Monad.Error
import Control.Monad.State
import qualified Data.ByteString.Char8 as B
data ParseError = NumericOverflow
| EndOfInput
| Chatty String
deriving (Eq, Ord, Show)
instance Error ParseError where
noMsg = Chatty "oh noes!"
strMsg = Chatty
對(duì)于我們解析器的狀態(tài),我們會(huì)創(chuàng)建一個(gè)非常小的monad變換器棧。一個(gè) State monad包含了需要解析的 ByteString ,在棧的頂部是 ErrorT 用于提供錯(cuò)誤處理。
-- file: ch19/ParseInt.hs
newtype Parser a = P {
runP :: ErrorT ParseError (State B.ByteString) a
} deriving (Monad, MonadError ParseError)
和平常一樣,我們將我們的monad棧包裝在一個(gè) newtype 中。這樣做沒(méi)有任意性能損耗,但是增加了類(lèi)型安全。我們故意避免繼承 MonadStateB.ByteString 的實(shí)例。這意味著 Parser monad用戶將不能夠使用 get 或者 put 去查詢或者修改解析器的狀態(tài)。這樣的結(jié)果是,我們強(qiáng)制自己去做一些手動(dòng)提升的事情來(lái)獲取在我們棧中的 State monad。
-- file: ch19/ParseInt.hs
liftP :: State B.ByteString a -> Parser a
liftP m = P (lift m)
satisfy :: (Char -> Bool) -> Parser Char
satisfy p = do
s <- liftP get
case B.uncons s of
Nothing -> throwError EndOfInput
Just (c, s')
| p c -> liftP (put s') >> return c
| otherwise -> throwError (Chatty "satisfy failed")
catchError 函數(shù)對(duì)于我們的任何非常有用,遠(yuǎn)勝于簡(jiǎn)單的錯(cuò)誤處理。例如,我們可以很輕松地解除一個(gè)異常,將它變成更友好的形式。
-- file: ch19/ParseInt.hs
optional :: Parser a -> Parser (Maybe a)
optional p = (Just `liftM` p) `catchError` \_ -> return Nothing
我們的執(zhí)行函數(shù)僅僅是將各層連接起來(lái),將結(jié)果重新組織成更整潔的形式。
-- file: ch19/ParseInt.hs
runParser :: Parser a -> B.ByteString
-> Either ParseError (a, B.ByteString)
runParser p bs = case runState (runErrorT (runP p)) bs of
(Left err, _) -> Left err
(Right r, bs) -> Right (r, bs)
如果我們將它加載到 ghci 中,我們可以對(duì)它進(jìn)行了一些測(cè)試。
ghci> :m +Data.Char
ghci> let p = satisfy isDigit
Loading package array-0.1.0.0 ... linking ... done.
Loading package bytestring-0.9.0.1 ... linking ... done.
Loading package mtl-1.1.0.0 ... linking ... done.
ghci> runParser p (B.pack "x")
Left (Chatty "satisfy failed")
ghci> runParser p (B.pack "9abc")
Right ('9',"abc")
ghci> runParser (optional p) (B.pack "x")
Right (Nothing,"x")
ghci> runParser (optional p) (B.pack "9a")
Right (Just '9',"a")
注
更多建議: