<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                合規國際互聯網加速 OSASE為企業客戶提供高速穩定SD-WAN國際加速解決方案。 廣告
                # 第十九章: 錯誤處理 無論使用哪門語言,錯誤處理都是程序員最重要–也是最容易忽視–的話題之一。在Haskell中,你會發現有兩類主流的錯誤處理:“純”的錯誤處理和異常。 當我們說“純”的錯誤處理,我們是指算法不依賴任何IO Monad。我們通常會利用Haskell富于表現力的數據類型系統來實現這一類錯誤處理。Haskell也支持異常。由于惰性求值復雜性,Haskell中任何地方都可能拋出異常,但是只會在IO monad中被捕獲。在這一章中,這兩類錯誤處理我們都會考慮。 ## 使用數據類型進行錯誤處理 讓我們從一個非常簡單的函數來開始我們關于錯誤處理的討論。假設我們希望對一系列的數字執行除法運算。分子是常數,但是分母是變化的。可能我們會寫出這樣一個函數: ~~~ -- file: ch19/divby1.hs divBy :: Integral a => a -> [a] -> [a] divBy numerator = map (numerator `div`) ~~~ 非常簡單,對吧?我們可以在 ghci 中執行這些代碼: ~~~ ghci> divBy 50 [1,2,5,8,10] [50,25,10,6,5] ghci> take 5 (divBy 100 [1..]) [100,50,33,25,20] ~~~ 這個行為跟我們預期的是一致的:50 / 1 得到50,50 / 2 得到25,等等。甚至對于無窮的鏈表 [1..] 它也是可以工作的。如果有個0溜進去我們的鏈表中了,會發生什么事呢? ~~~ ghci> divBy 50 [1,2,0,8,10] [50,25,*** Exception: divide by zero ~~~ 是不是很有意思? ghci 開始顯示輸出,然后當它遇到零時發生了一個異常停止了。這是惰性求值的作用–它只按需求值。 在這一章里接下來我們會看到,缺乏一個明確的異常處理時,這個異常會使程序崩潰。這當然不是我們想要的,所以讓我們思考一下更好的方式來表征這個純函數中的錯誤。 ## 使用Maybe 可以立刻想到的一個表示失敗的簡單的方法是使用 Maybe 。如果輸入鏈表中任何地方包含了零,相對于僅僅返回一個鏈表并在失敗的時候拋出異常,我們可以返回 Nothing ,或者如果沒有出現零我們可以返回結果的 Just。下面是這個算法的實現: ~~~ -- 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 中嘗試它,你會發現它可以工作: ~~~ ghci> divBy 50 [1,2,5,8,10] Just [50,25,10,6,5] ghci> divBy 50 [1,2,0,8,10] Nothing ~~~ 調用 divBy 的函數現在可以使用 case 語句來觀察調用成功與否,就像 divBy 調用自己時所做的那樣。 Tip 你大概注意到,上面可以使用一個monadic的實現,像這樣子: ~~~ -- 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 ~~~ 出于簡單考慮,在這章中我們會避免使用monadic實現,但是會指出有這種做法。 [譯注:Tip中那段代碼編譯不過] #### 丟失和保存惰性 使用 Maybe 很方便,但是有代價。 divBy 將不能夠再處理無限的鏈表輸入。由于結果是一個 Maybe[a] ,必須要檢查整個輸入鏈表,我們才能確認不會因為存在零而返回 Nothing 。你可以嘗試在之前的例子中驗證這一點: ~~~ ghci> divBy 100 [1..] *** Exception: stack overflow ~~~ 這里觀察到,你沒有看到部分的輸出;你沒得到任何輸出。注意到在 divBy 的每一步中(除了輸入鏈表為空或者鏈表開頭是零的情況),每個子序列元素的結果必須先于當前元素的結果得到。因此這個算法無法處理無窮鏈表,并且對于大的有限鏈表,它的空間效率也不高。 之前已經說過, Maybe 通常是一個好的選擇。在這個特殊例子中,只有當我們去執行整個輸入的時候我們才知道是否有問題。有時候我們可以提交發現問題,例如,在 ghci 中 tail[] 會生成一個異常。我們可以很容易寫一個可以處理無窮情況的 tail : ~~~ -- file: ch19/safetail.hs safeTail :: [a] -> Maybe [a] safeTail [] = Nothing safeTail (_:xs) = Just xs ~~~ 如果輸入為空,簡單的返回一個 Nothing ,其它情況返回結果的 Just 。由于在知道是否發生錯誤之前,我們只需要確認鏈表非空,在這里使用 Maybe 不會破壞惰性。我們可以在 ghci 中測試并觀察跟普通的 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 執行結果符合預期。但是對于無窮鏈表呢?我們不想打印無窮的結果的數字,所以我們用 take5(tail[1..]) 以及一個類似的saftTail構建測試: ~~~ 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 都可以處理無窮鏈表。注意我們可以更好地處理空的輸入鏈表;而不是拋出異常,我們決定這種情況返回 Nothing 。我們可以獲得錯誤處理能力卻不會失去惰性。 但是我們如何將它應用到我們的 divBy 的例子中呢?讓我們思考下現在的情況:失敗是單個壞的輸入的屬性,而不是輸入鏈表自身。那么將失敗作為單個輸出元素的屬性,而不是整個輸出鏈表怎么樣?也就是說,不是一個類型為 a->[a]->Maybe[a] 的函數,取而代之我們使用 a->[a]->[Maybea] 。這樣做的好處是可以保留惰性,并且調用者可以確定是在鏈表中的哪里出了問題–或者甚至是過濾掉有問題的結果,如果需要的話。這里是一個實現: ~~~ -- 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) ~~~ 看下這個函數,我們再次回到使用 map ,這無論對簡潔和惰性都是件好事。我們可以在 ghci 中測試它,并觀察對于有限和無限鏈表它都可以正常工作: ~~~ 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] ~~~ 我們希望通過這個討論你可以明白這點,不符合規范的(正如 safeTail 中的情況)輸入和包含壞的數據的輸入( divBy 中的情況)是有區別的。這兩種情況通常需要對結果采用不同的處理。 #### Maybe Monad的用法 回到 *使用Maybe* 這一節,我們有一個叫做 divby2.hs 的示例程序。這個例子沒有保存惰性,而是返回一個類型為 Maybe[a] 的值。用monadic風格也可以表達同樣的算法。更多信息和monad相關背景,參考 [第14章Monads](http://rwh.readthedocs.org/en/latest/chp/14.html) [http://rwh.readthedocs.org/en/latest/chp/14.html] 。這是我們新的monadic風格的算法: ~~~ -- 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使得這個算法的表示看上去更好。對于 Maybe monad, return 就跟 Just 一樣,并且 fail_=Nothing ,因此我們看到任何的錯誤說明的字段串。我們可以用我們在 divby2.hs 中使用過的測試來測試這個算法: ~~~ 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 ~~~ 我們寫的代碼實際上并不限于 Maybe monad。只要簡單地改變類型,我們可以讓它對于任何monad都能工作。讓我們試一下: ~~~ -- 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) ~~~ 函數 divByGeneric 包含的代碼 divBy 之前所做的一樣;我們只是給它一個更通用的類型。事實上,如果不給出類型,這個類型是由 ghci 自動推導的。我們還為特定的類型定義了一個更方便的函數 divBy 。 讓我們在 ghci 中運行一下。 ~~~ 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) ~~~ 前兩個例子產生的輸出都跟我們之前看到的一樣。由于 divByGeneric 沒有指定返回的類型,我們要么指定一個,要么讓解釋器從環境中推導得到。如果我們不指定返回類型, ghic 推薦得到 IO monad。在第三和第四個例子中你可以看出來。在第四個例子中你可以看到, IO monad將 fail 轉化成了一個異常。 mtl 包中的 Control.Monad.Error 模塊也將 EitherString 變成了一個monad。如果你使用 Either ,你可以得到保存了錯誤信息的純的結果,像這樣子: ~~~ 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" ~~~ 這讓我們進入到下一個話題的討論:使用 Either 返回錯誤信息。 ## 使用Either Either 類型跟 Maybe 類型類似,除了一處關鍵的不同:對于錯誤或者成功(“ Right 類型”),它都可以攜帶數據。盡管語言沒有強加任何限制,按照慣例,一個返回 Either 的函數使用 Left 返回值來表示一個錯誤, Right 來表示成功。如果你覺得這樣有助于記憶,你可以認為 Right 表式正確結果。我們可以改一下前面小節中關于 Maybe 時使用的 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 的代碼幾乎是完全一樣的;我們只是把每個 Just 用 Right 替換。Left 對應于 Nothing ,但是現在它可以攜帶一條信息。讓我們在 ghci 里面運行一下: > 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 類型來表示錯誤的原因對今后很有好處,自定義的錯誤類型通常會更有幫助。使用自定義的錯誤類型我們可以知道到底是出了什么問題,并且獲知是什么動作引發的這個問題。例如,讓我們假設,由于某些原因,不僅僅是除0,我們還不想除以10或者20。我們可以像這樣子自定義一個錯誤類型: ~~~ -- 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) ~~~ 現在,在出現錯誤時,可以通過 Left 數據檢查導致錯誤的準確原因。或者,可以簡單的只是通過 show 打印出來。下面是這個函數的應用: ~~~ 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 一樣,都會遇到失去惰性的問題。我們將在這一章的最后用一個練習題來解決這個問題。 #### Monadic地使用Either 回到 *Maybe Monad的用法* 這一節,我們向你展示了如何在一個monad中使用 Maybe 。 Either 也可以在monad中使用,但是可能會復雜一點。原因是 fail 是硬編碼的只接受 String 作為失敗代碼,因此我們必須有一種方法將這樣的字符串映射成我們的 Left 使用的類型。正如你前面所見, Control.Monad.Error 為 EitherStringa 提供了內置的支持,它沒有涉及到將參數映射到 fail 。這里我們可以將我們的例子修改為monadic風格使得 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) ~~~ 這里,我們需要打開 FlexibleContexts 語言擴展以提供 divByGeneric 的類型簽名。 divBy 函數跟之前的工作方式完全一致。對于 divByGeneric ,我們將 divByError 做為 Error 類型類的成員,通過定義調用 fail 時的行為( strMsg 函數)。我們還將 Right 轉化成 return ,將 Left 轉化成 throwError 進行泛化。 ## 異常 許多語言中都有異常處理,包括Haskell。異常很有用,因為當發生故障時,它提供了一種簡單的處理方法,即使故障離發生的地方沿著函數調用鏈走了幾層。有了異常,不需要檢查每個函數調用的返回值是否發生了錯誤,不需要注意去生成表示錯誤的返回值,像C程序員必須這么做。在Haskell中,由于有 monad以及 Either 和 Maybe 類型,你通常可以在純的代碼中達到同樣的效果而不需要使用異常和異常處理。 有些問題–尤其是涉及到IO調用–需要處理異常。在Haskell中,異常可能會在程序的任何地方拋出。然而,由于計算順序是不確定的,異常只可以在 IO monad中捕獲。Haskell異常處理不涉及像Python或者Java中那樣的特殊語法。捕獲和處理異常的技術是–真令人驚訝–函數。 ## 異常第一步 在 Control.Exception 模塊中,定義了各種跟異常相關的函數和類型。 Exception 類型是在那里定義的;所有的異常的類型都是 Exception 。還有用于捕獲和處理異常的函數。讓我們先看一看 try ,它的類型是 IOa->IO(EitherExceptiona) 。它將異常處理包裝在 IO 中。如果有異常拋出,它會返回一個 Left 值表示異常;否則,返回原始結果到 Right 值。讓我們在 ghci 中運行一下。我們首先觸發一個未處理的異常,然后嘗試捕獲它。 ~~~ 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 語句中沒有拋出異常。這是意料之中的,是因為惰性求值;除以零只有到打印 x 的值的時候才需要計算。還有,注意 try(printy) 有兩行輸出。第一行是由 print 產生的,它在終端上顯示5。第二個是由 ghci 生成的,這個表示 printy 的返回值為 () 并且沒有拋出異常。 ## 惰性和異常處理 既然你知道了 try 是如何工作的,讓我們試下另一個實驗。讓我們假設我們想捕獲 try 的結果用于后續的計算,這樣我們可以處理除的結果。我們大概會這么做: ~~~ ghci> result <- try (return x) Right *** Exception: divide by zero ~~~ 這里發生了什么?讓我們拆成一步一步看,先試下另一個例子: ~~~ ghci> let z = undefined ghci> try (print z) Left Prelude.undefined ghci> result <- try (return z) Right *** Exception: Prelude.undefined ~~~ 跟之前一樣,將 undefined 賦值給 z 沒什么問題。問題的關鍵,以及前面的迷惑,都在于惰性求值。準確地說,是在于 return ,它沒有強制它的參數的執行;它只是將它包裝了一下。這樣, try(returnundefined) 的結果應該是 Rightundefined 。現在, ghci 想要將這個結果顯示在終端上。它將運行到打印”Right”,但是 undefined 無法打印(或者說除以零的結果無法打印)。因此你看到了異常信息,它是來源于 ghci 的,而不是你的程序。 這是一個關鍵點。讓我們想想為什么之前的例子可以工作,而這個不可以。之前,我們把 printx 放在了 try 里面。打印一些東西的值,固然是需要執行它的,因此,異常在正確的地方被檢測到了。但是,僅僅是使用 return 并不會強制計算的執行。為了解決這個問題, Control.Exception 模塊中定義了一個 evaluate 函數。它的行為跟 return 類似,但是會讓參數立即執行。讓我們試一下: ~~~ ghci> let z = undefined ghci> result <- try (evaluate z) Left Prelude.undefined ghci> result <- try (evaluate x) Left divide by zero ~~~ 看,這就是我們想要的答案。無論對于 undefiined 還是除以零的例子,都可以正常工作。 Tip 記住:任何時候你想捕獲純的代碼中拋出的異常,在你的異常處理函數中使用 evaluate 而不是 return 。 ## 使用handle 通常,你可能希望如果一塊代碼中沒有任何異常發生,就執行某個動作,否則執行不同的動作。對于像這種場合,有一個叫做 handle 的函數。這個函數的類型是 (Exception->IOa)->IOa->IOa 。即是說,它需要兩個參數:前一個是一個函數,當執行后一個動作發生異常的時候它會被調用。下面是我們使用的一種方式: ~~~ 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 ~~~ 像這樣,如果計算中沒有錯誤發生,我們可以打印一條好的信息。這當然要比除以零出錯時程序崩潰要好。 ## 選擇性地處理異常 上面的例子的一個問題是,對于任何異常它都是打印 “Error calculating result”。可能會有些其它不是除零的異常。例如,顯示輸出時可能會發生錯誤,或者純的代碼中可能拋出一些其它的異常。 handleJust 函數就是處理這種情況的。它讓你指定一個測試來決定是否對給定的異常感興趣。讓我們看一下: ~~~ -- 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 定義了一個函數,這個函數會決定我們對給定的異常是否感興趣。如果是,它會返回 Just ,否則返回 Nothing 。還有, Just 中附帶的值會被傳到我們的處理函數中。現在我們可以很好地使用 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 中使用的函數,以便于我們將異常的范圍縮小到我們所關心的類別。例如,有個函數 arithExceptions 類型是 Exception->MaybeArithException 可以挑選出任意的 ArithException 異常,但是會忽略掉其它。我們可以像這樣使用它: ~~~ -- 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 類型的異常,但是仍然讓其它的異常通過,不捕獲也不修改。我們可以看到它是這樣工作的: ~~~ 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 測試,這是跟一大類的I/O相關的異常。 ## I/O異常 大概在任何程序中異常最大的來源就是I/O。在處理外部世界的時候所有事情都可能出錯:磁盤滿了,網絡斷了,或者你期望文件里面有數據而文件卻是空的。在Haskell中,I/O異常就跟其它的異常一樣可以用 Exception 數據類型來表示。另一方面,由于有這么多類型的I/O異常,有一個特殊的模塊– System.IO.Error 專門用于處理它們。 System.IO.Error 定義了兩個函數: catch 和 try ,跟 Control.Exception 中的類似,它們都是用于處理異常的。然而,不像 Control.Exception 中的函數,這些函數只會捕獲I/O錯誤,而不處理其它類型異常。在Haskell中,所有I/O錯誤有一個共同類型 IOError ,它的定義跟 IOException 是一樣的。 Tip 當心你使用的哪個名字因為 System.IO.Error 和 Control.Exception 定義了同樣名字的函數,如果你將它們都導入你的程序,你將收到一個錯誤信息說引用的函數有歧義。你可以通過 qualified 引用其中一個或者另一個,或者將其中一個或者另一個的符號隱藏。 注意 Prelude 導出的是 System.IO.Error 中的 catch ,而不是 ControlException 中提供的。記住,前者只捕獲I/O錯誤,而后者捕獲所有的異常。換句話說, 你要的幾乎總是 Control.Exception 中的那個 catch ,而不是默認的那個。 讓我們看一下對我們有益的一個在I/O系統中使用異常的方法。在 [使用文件和句柄](http://rwh.readthedocs.org/en/latest/chp/7.html#handle) [http://rwh.readthedocs.org/en/latest/chp/7.html#handle] 這一節里,我們展示了一個使用命令式風格從文件中一行一行的讀取的程序。盡管我們后面也示范過更簡潔的,更”Haskelly”的方式解決那個問題,讓我們在這里重新審視這個例子。在 mainloop 函數中,在讀一行之前,我們必須明確地測試我們的輸入文件是否結束。這次,我們可以檢查嘗試讀一行是否會導致一個EOF錯誤,像這樣子: ~~~ -- 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 來檢測是否 hGetLine 拋出一個 IOError 。如果是,我們使用 isEOFError (在 System.IO.Error 中定義)來看是否拋出異常表明我們到達了文件末尾。如果是的,我們退出循環。如果是其它的異常,我們調用 ioError 重新拋出它。 有許多的這種測試和方法可以從 System.IO.Error 中定義的 IOError 中提取信息。我們推薦你在需要的時候去查一下庫的參考頁。 ## 拋出異常 到現在為止,我們已經詳細地討論了異常處理。還有另外一個困惑:拋出異常。到目前為止這一章我們所接觸到的例子中,都是由Haskell為你拋出異常的。然后你也可以自己拋出任何異常。我們會告訴你怎么做。 你將會注意到這些函數大部分似乎返回一個類型為 a 或者 IOa 的值。這意味著這個函數似乎可以返回任意類型的值。事實上,由于這些函數會拋出異常,一般情況下它們決不“返回”任何東西。這些返回值讓你可以在各種各樣的上下文中使用這些函數,不同的上下文需要不同的類型。 讓我們使用函數 Control.Exception 來開始我們的拋出異常的教程。最通用的函數是 throw ,它的類型是 Exception->a 。這個函數可以拋出任何的 Exception ,并且可以用于純的上下文中。還有一個類型為 Exception->IOa 的函數 throwIO 在 IO monad中拋出異常。這兩個函數都需要一個 Exception 用于拋出。你可以手工制作一個 Exception ,或者重用之前創建的 Exception 。 還有一個函數 ioError ,它在 Control.Exception 和 System.IO.Error 中定義都是相同的,它的類型是 IOError->IOa 。當你想生成任意的I/O相關的異常的時候可以使用它。 ## 動態異常 這需要使用兩個很不常用的Haskell模塊: Data.Dynamic 和 Data.Typeable 。我們不會講太多關于這些模塊,但是告訴你當你需要制作自己的動態異常類型時,可以使用這些工具。 在 [第二十一章使用數據庫http://book.realworldhaskell.org/read/using-databases.html](#) 中,你會看到HDBC數據庫庫使用動態異常來表示SQL數據庫返回給應用的錯誤。數據庫引擎返回的錯誤通常有三個組件:一個表示錯誤碼的整數,一個狀態,以及一條人類可讀的錯誤消息。在這一章中我們會創建我們自己的HDBC SqlError 實現。讓我們從錯誤自身的數據結構表示開始: ~~~ -- 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) ~~~ 通過繼承 Typeable 類型類,我們使這個類型可用于動態的類型編程。為了讓GHC自動生成一個 Typeable 實例,我們要開啟 DeriveDataTypeable 語言擴展。 現在,讓我們定義一個 catchSql 和一個 handleSql 用于捕獵一個 SqlError 異常。注意常規的 catch 和 handle 函數無法捕獵我們的 SqlError ,因為它不是 Exception 類型的。 ~~~ -- 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] 這些函數僅僅是在 catchDyn 外面包了很薄的一層,類型是 Typeableexception=>IOa->(exception->IOa)->IOa 。這里我們簡單地限定了它的類型使得它只捕獵SQL異常。 正常地,當一個異常拋出,但是沒有在任何地方被捕獲,程序會崩潰并顯示異常到標準錯誤輸出。然而,對于動態異常,系統不會知道該如何顯示它,因此你將僅僅會看到一個的”unknown exception”消息,這可能沒太大幫助。我們可以提供一個輔助函數,這樣應用可以寫成,比如說 main=handleSqlError$do... ,使拋出的異常可以顯示。下面是如何寫 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,這里重命名過文件] 最后,讓我們給出一個如何拋出 SqlError 異常的例子。下面的函數做的就是這件事: ~~~ -- 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 類似但是會立即計算它的參數。 這樣我們的動態異常的支持就完成了。代碼很多,你大概不需要這么多代碼,但是我們想要給你一個動態異常自身的例子以及和它相關的工具。事實上,這里的例子幾乎就反映在HDBC庫中。讓我們在 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錯誤。但是,你可以看到 handleSqlError 幫助做了這些,不過沒有捕獲其它的錯誤。最后讓我們試一個自定義的handler: ~~~ ghci> handleSql (fail . seErrorMsg) (throwSqlErrorIO "state" 5 "my error") *** Exception: user error (my error) ~~~ 這里,我們自定義了一個錯誤處理拋出一個新的異常,構成 SqlError 中的 seErrorMsg 域。你可以看到它是按預想中那樣工作的。 ## 練習 1. 將 Either 修改成 Maybe 例子中的那種風格,使它保存惰性。 ## monad中的錯誤處理 因為我們必須捕獲 IO monad中的異常,如果我們在一個monad中或者在monad的轉化棧中使用它們,我們將跳出到 IO monad。這幾乎肯定不是我們想要的。 在 [構建以理解Monad變換器](http://rwh.readthedocs.org/en/latest/chp/18.html#id9) [http://rwh.readthedocs.org/en/latest/chp/18.html#id9] 中我們定義了一個 MaybeT 的變換,但是它更像是一個有助于理解的東西,而不是編程的工具。幸運的是,已經有一個專門的–也更有用的–monad變換: ErrorT ,它是定義在 Control.Monad.Error 模塊中的。 ErrorT 變換器使我們可以向monad中添加異常,但是它使用了特殊的方法,跟 Control.Exception 模塊中提供的不一樣。它提供給我們一些有趣的能力。 - 如果我們繼續用 ErrorT 接口,在這個monad中我們可以拋出和捕獲異常。 - 根據其它monad變換器的命名規范,這個執行函數的名字是 runErrorT 。當它遇到 runErrorT 之后,未被捕獲的 ErrorT 異常將停止向上傳遞。我們不會被踢到 IO monad中。 - 我們可以控制我們的異常的類型。 Warning 不要把ErrorT跟普通異常混淆如果我們在 ErrorT 內面使用 Control.Exception 中的 throw 函數,我們仍然會彈出到 IO monad。 正如其它的 mtl monad一樣, ErrorT 提供的接口是由一個類型類定義的。 ~~~ -- 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 ~~~ 類型變量 e 代表我們想要使用的錯誤類型。不管我們的錯誤類型是什么,我們必須將它做成 Error 類型類的實例。 ~~~ -- 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 實現 fail 時會用到 strMsg 函數。它將 strMsg 作為一個異常拋出,將自己接收到的字符串參數傳遞給這個異常。對于 noMsg ,它是用于提供 MonadPlus 類型類中的 mzero 的實現。 為了支持 strMsg 和 noMsg 函數,我們的 ParseError 類型會有一個 Chatty 構造器。這個將用作構造器如果,比如說,有人在我們的monad中調用 fail 。 我們需要知道的最后一塊是關于執行函數 runErrorT 的類型。 ~~~ ghci> :t runErrorT runErrorT :: ErrorT e m a -> m (Either e a) ~~~ ## 一個小的解析構架 為了說明 ErrorT 的使用,讓我們開發一個類似于Parsec的解析庫的基本的骨架。 ~~~ -- 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 ~~~ 對于我們解析器的狀態,我們會創建一個非常小的monad變換器棧。一個 State monad包含了需要解析的 ByteString ,在棧的頂部是 ErrorT 用于提供錯誤處理。 ~~~ -- file: ch19/ParseInt.hs newtype Parser a = P { runP :: ErrorT ParseError (State B.ByteString) a } deriving (Monad, MonadError ParseError) ~~~ 和平常一樣,我們將我們的monad棧包裝在一個 newtype 中。這樣做沒有任意性能損耗,但是增加了類型安全。我們故意避免繼承 MonadStateB.ByteString 的實例。這意味著 Parser monad用戶將不能夠使用 get 或者 put 去查詢或者修改解析器的狀態。這樣的結果是,我們強制自己去做一些手動提升的事情來獲取在我們棧中的 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 函數對于我們的任何非常有用,遠勝于簡單的錯誤處理。例如,我們可以很輕松地解除一個異常,將它變成更友好的形式。 ~~~ -- file: ch19/ParseInt.hs optional :: Parser a -> Parser (Maybe a) optional p = (Just `liftM` p) `catchError` \_ -> return Nothing ~~~ 我們的執行函數僅僅是將各層連接起來,將結果重新組織成更整潔的形式。 ~~~ -- 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 中,我們可以對它進行了一些測試。 ~~~ 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") ~~~ ## 級習 1. 寫一個 many 解析器,類型是 Parsera->Parser[a] 。它應該執行解析直到失敗。 1. 使用 many 寫一個 int 解析器,類型是 ParserInt 。它應該既能接受負數也能接受正數。 1. 修改你們 int 解析器,如果在解析時檢測到了一個數值溢出,拋出一個 NumericOverflow 異常。 注 | [38] | 這里我們使用的是整數的除法,因此 50 / 8 顯示是 6 而不是 6.25 。在這個例子中我們沒有使用浮點算術是因為對一個 Double 除以零會返回一個特殊的 Infinity 而不是一個錯誤。 | |-----|-----| | [39] | 關于 Maybe 的介紹,參考`<讓過程更可控的方法 [http://rwh.readthedocs.org/en/latest/chp/3.html#id21](http://rwh.readthedocs.org/en/latest/chp/3.html#id21)>`_ | |-----|-----| | [40] | 更多關于 Either 的信息,參考`<通過 API 設計進行錯誤處理 [http://rwh.readthedocs.org/en/latest/chp/8.html#api](http://rwh.readthedocs.org/en/latest/chp/8.html#api)>`_ | |-----|-----| | [41] | 在一些其它語言中,拋出異常是叫做 raising 。 | |-----|-----| | [42] | 可以手動繼承 Typeable 實例,但是那樣很麻煩。 | |-----|-----|
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看