在〈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 String
,FilePath
只是 String
的別名,你可以指定檔案路徑,它會讀取檔案的內容。
指定的檔案存在時,這個程式會顯示檔案內容,指定的檔案不存在時,就會發生錯誤:
你可以使用 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.Typeable
的 typeOf
函式,以得知實際的 Exception
型態,例如指定檔案不存在時會發生 IOException
:
將嘗試處理的函式與錯誤處理的函式分開定義,會是比較易讀的寫法,例如:
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)]
那麼,既然 finally
是 bracket
的特化版本,就請你試著用 bracket
來實現 finally
當作練習吧!