Haskell Tutorial(23)Exception 的 catch 與 handle


在〈Haskell Tutorial(22)Maybe 有無、Either 對錯〉中,看過 try 的使用,也談到了在純綷的世界中拋出 Exception 這件事並不建議,因為你很難掌握函式實際被執行的時間,在非純綷世界中拋出 Exception 合理的多,因為總是會有意外狀況,例如,你可能讓使用者輸入檔案名稱並讀取檔案:

import System.Environment
import System.IO

main = do
    (fileName:_) <- getArgs
    contents <- readFile fileName
    putStrLn contents

來自 System.Environment 模組的 getArgs 函式,型態是 getArgs :: IO [String],其中 List 是使用者執行程式時給定的命令列引數;System.IO 中的 readFile 函式,型態是 readFile :: FilePath -> IO StringFilePath 只是 String 的別名,你可以指定檔案路徑,它會讀取檔案的內容。

指定的檔案存在時,這個程式會顯示檔案內容,指定的檔案不存在時,就會發生錯誤:

IOException

你可以使用 doesFileExist 函式,來事先檢查檔案是否存在,以避免這個錯誤,不過,這篇的主題是談 Exception 處理,先來看看如何使用 catch 函式處理 …

Exception 的 catch

System.IO.Error 中原本定義有 catch 函式,並導出至 Prelude 模組,不過現在已不建議使用,這邊使用的是 Control.Exception 中的 catch 函式,它的型態是 Exception e => IO a -> (e -> IO a) -> IO a,也就是接受一個 IO 動作結果與一個可處理 Exception 的函式,最後傳回一個 IO 動作結果。

來看看如何將上面讀取檔案的例子,使用 catch 來處理錯誤:

import Data.Typeable
import Prelude hiding (catch)
import Control.Exception
import System.Environment
import System.IO

main = catch
    (do (fileName:_) <- getArgs
        contents <- readFile fileName
        putStrLn contents)
    (\(SomeException e) -> do print $ typeOf e
                              print e)

這個程式使用 catch 來執行檔案讀取,如果發生 Exception 的話,會使用另一個指定的函式來處理錯誤,在這邊使用了 Data.TypeabletypeOf 函式,以得知實際的 Exception 型態,例如指定檔案不存在時會發生 IOException

catch 函式

將嘗試處理的函式與錯誤處理的函式分開定義,會是比較易讀的寫法,例如:

import Prelude hiding (catch)
import Control.Exception
import System.Environment
import System.IO

main = tryShowContent `catch` handleIOException

tryShowContent :: IO ()
tryShowContent = do
    (fileName:_) <- getArgs
    contents <- readFile fileName
    putStrLn contents

handleIOException :: IOException -> IO ()
handleIOException e = do
   putStrLn "發生 IOException ..."
   print e

在〈Haskell Tutorial(22)Maybe 有無、Either 對錯〉看過的 try 函式,實際上可基於 catch 實作,將捕捉到的 Exception 使用 Either 傳回:

import Control.Exception

try' :: Exception e => IO a -> IO (Either e a)
try' a = catch toEither (return . Left)
    where toEither = do
              r <- a
              return (Right r)
main = do
    result <- try' (evaluate (head []))
    case result :: Either SomeException Int of
        Left  ex -> putStrLn $ "發生 Exception:" ++ show ex
        Right ele -> putStrLn $ "答案:" ++ show ele

多個 Exception 的 catch

有時候,你會想要捕捉多個 Exception,例如:

import Prelude hiding (catch)
import Control.Exception
import System.Environment
import System.IO

main =
    (do (fileName:_) <- getArgs
        contents <- readFile fileName
        putStrLn contents)
    `catch` (\(_::IOException) -> putStrLn "發生 IOException ...")
    `catch` (\(e::SomeException) -> print $ typeOf e)

這邊特意標示出 Exception 的型態,讓它看起來像是 Java 的 catch 語法,因為在函式的參數上直接標示了型態,為了要通過編譯,編譯時得加上 -XScopedTypeVariables

這個方式乍看行得通,不過有點問題,因為這是將前一個 catch 的結果,作為第二個 catch 的第一個引數,因此,前一個 catch 中若發生了 Exception 而第一個處理 Exception 的函式中又拋出了 Exception,那麼下一個 catch 處理 Exception 的函式就會捕捉到它,這顯然與其他語言中處理 Exception 的行為不太一樣。

這時你可以改用 catches 函式,它的型態是 IO a -> [Handler a] -> IO a,其中 Handler 型態有個值建構式 Handler,型態為 Exception e => (e -> IO a) -> Handler a,來看看如何使用:

import Data.Typeable
import Prelude hiding (catch)
import Control.Exception
import System.Environment
import System.IO

main =
    (do (fileName:_) <- getArgs
        contents <- readFile fileName
        putStrLn contents)
    `catches`
        [Handler (\(_::IOException) -> putStrLn "發生 IOException ..."),
         Handler (\(e::SomeException) -> print $ typeOf e)]

Exception 的 handle

handle 函式的型態為 Exception e => (e -> IO a) -> IO a -> IO a,與 try 函式相比,只是引數順序不同,也就是處理 Exception 的函式會是第一個引數,可基於可讀性來選擇使用 try 或使用 handle,通常在處理 Exception 的函式簡短的情況下,可選擇使用 handler,例如:

import Prelude hiding (catch)
import Control.Exception
import System.Environment
import System.IO

main = handle (\(SomeException _) -> putStrLn "發生 IOException ...")
       (do (fileName:_) <- getArgs
           contents <- readFile fileName
           putStrLn contents)

finally?

熟悉具有 Exception 處理機制的語言,像是 Java 等的開發者,都知道會有個 finally,可用來做一些資源善後工作,Haskell 中也有個 finally 函式,型態是 IO a -> IO a -> IO a,如果特意模彷 Java 的 Exception 語法,可以如下撰寫:

import Data.Typeable
import Prelude hiding (catch)
import Control.Exception
import System.Environment
import System.IO

main =
    (do (fileName:_) <- getArgs
        contents <- readFile fileName
        putStrLn contents)
    `catches`
        [Handler (\(_::IOException) -> putStrLn "發生 IOException ..."),
         Handler (\(e::SomeException) -> print $ typeOf e)]
    `finally` (putStrLn "finally 執行最後資源善後")

實際上,finally 函式只是 Haskell 中 bracket 函式的特化,對於自行開檔與關檔這類動作時,使用 bracket 會比較方便,例如可使用 bracket 自行實作 readFile 函式:

import Data.Typeable
import Prelude hiding (catch, readFile)
import Control.Exception
import System.Environment
import System.IO hiding (readFile)

readFile :: FilePath -> IO String
readFile fileName = 
    bracket (openFile fileName ReadMode) hClose (\handle -> do
        contents <- hGetContents handle
        evaluate contents
        return contents)

main =
    (do (fileName:_) <- getArgs
        contents <- readFile fileName
        putStrLn contents)
    `catches`
        [Handler (\(_::IOException) -> putStrLn "發生 IOException ..."),
         Handler (\(e::SomeException) -> print $ typeOf e)]

那麼,既然 finallybracket 的特化版本,就請你試著用 bracket 來實現 finally 當作練習吧!