try/catch/finally
February 11, 2022在〈Maybe 有無、Either 對錯〉談過,head [] 會拋出 Exception:
ghci> head []
*** Exception: Prelude.head: empty list
ghci>
也談到因為惰性的關係,你不會知道何時會噴出了 Exception,這麼一來,處理 Exception 的時機是個麻煩,因為惰性的關係,head [] 不會馬上執行:
ghci> let x = head []
ghci>
try 函式
如果要處理 head 的 Exception,可以使用 Control.Exception 的 try 函式:
import Control.Exception
main = do
result <- (try $ return $ head [1, 2, 3]) :: IO (Either SomeException Int)
case result of
Left e -> putStrLn $ "發生 Exception:" ++ show e
Right elem -> putStrLn $ "首元素:" ++ show elem
try 的型態是 Exception e => IO a -> IO (Either e a),它接受 IO a,在範例中,對 head 執行結果使用 return,再作為 try 的引數,try 傳回一個 IO (Either e a),正確執行的話結果就是 a,發生錯誤的話,會捕捉 Exception 為 e。
這邊看到的 SomeException,具有 Exception 的行為,先用簡單的說法解釋的話,SomeException 為具有 Exception 行為的頂層型態,想瞭解階層系統如何定義,可以參考〈The Exception type〉。
如果執行上面這個程式,結果會顯示 "首元素:1",不過,試著將 [1, 2, 3] 改為 [] 並編譯執行,你會看到什麼?"發生 Exception:empty list"?不是!會看到 "Prelude.head: empty list",這是 Haskell 執行環境給你的訊息,不是你定義出來要顯示的訊息。
因為惰性的關係,head [] 執行的時機不一定在 try,Haskell 有個 evaluate 函式,可以用來要求立即執行函式,如果一個純函式可能拋出 Exception,想要透過 try 來處理,必須使用 evaluate 函式:
import Control.Exception
main = do
result <- (try $ evaluate $ head []) :: IO (Either SomeException Int)
case result of
Left e -> putStrLn $ "發生 Exception:" ++ show e
Right elem -> putStrLn $ "首元素:" ++ show elem
執行這個範例才會看到 "發生 Exception:Prelude.head: empty list" 的顯示結果。
從純綷的函式中拋出 Exception,並不是要讓你在執行時期嘗試處理,執行純函式時若發生了 Exception,往往表示呼叫函式的條件不足而發生的錯誤,你應該停止程式,檢視、修改程式碼,做好呼叫函式前的條件檢,別讓函式有機會拋出 Exception。
就上例而言,是呼叫 head 的時機不對,實際的應用程式中,是不會直接寫 head [],上例純綷只是為了示範 try;實際的應用程式中,不該讓 [] 有機會成為 head 的引數,應該使用 null 檢查,或者透過模式比對 [] 的可能性。
要在自訂的純函式中拋出 Exception,可以使用 error 函式,在〈Data.List/Set/Map 模組〉曾經看過,自行拋出 Exception 後續的文件還會談到。
try 的使用時機,主要是那些有副作用的函式,例如,來自 System.Environment 模組的 getArgs 函式,型態是 getArgs :: IO [String],其中 list 是使用者執行程式時給定的命令列引數,readFile 函式的型態是 readFile :: FilePath -> IO String,FilePath 只是 String 的別名,你可以指定檔案路徑,它會讀取檔案的內容。
以下範例可以從命令列引數讀取純文字檔案並顯示:
import System.Environment
main = do
(fileName:_) <- getArgs
contents <- readFile fileName
putStrLn contents
指定的檔案存在時,這個程式會顯示檔案內容,指定的檔案不存在時,就會發生錯誤,可以使用 System.Directory 的 doesFileExist 函式,事先檢查檔案是否存在以避免錯誤,如果想使用 try 函式來處理錯誤的話,可以如下:
import Control.Exception
import System.Environment
main = do
(fileName:_) <- getArgs
result <- (try $ readFile fileName) :: IO (Either IOException String)
case result of
Left e -> putStrLn $ "發生 Exception:" ++ show e
Right contents -> putStrLn $ "檔案內容:" ++ contents
catch 函式
Control.Exception 的 catch 函式,型態是 Exception e => IO a -> (e -> IO a) -> IO a,接受一個 IO 動作結果與一個可處理 Exception 的函式,最後傳回一個 IO 動作結果。
來看看如何將上面讀取檔案的例子,使用 catch 來處理錯誤:
import Data.Typeable
import Control.Exception
import System.Environment
main =
(do (fileName:_) <- getArgs
contents <- readFile fileName
putStrLn contents)
`catch` (\(e :: SomeException) -> do putStrLn $ show $ typeOf e
putStrLn $ show e)
catch 接受檔案讀取等 IO 動作,如果發生 Exception 的話,會使用另一個指定的函式來處理錯誤,在這邊使用了 Data.Typeable 的 typeOf 函式,以得知實際的 Exception 型態,例如指定檔案不存在時會發生 IOException:
> readFile some.txt
IOException
some.txt: openFile: does not exist (No such file or directory)
將嘗試處理的函式與錯誤處理的函式分開定義,會是比較易讀的寫法,例如:
import Control.Exception
import System.Environment
main = putStrLnContent `catch` putStrLnIOException
putStrLnContent :: IO ()
putStrLnContent = do
(fileName:_) <- getArgs
contents <- readFile fileName
putStrLn contents
putStrLnIOException :: IOException -> IO ()
putStrLnIOException e = do
putStrLn $ show e
方才看過的 try 函式,可基於 catch 實作,將捕捉到的 Exception 使用 Either 傳回:
import Control.Exception
import System.Environment
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
(fileName:_) <- getArgs
result <- (try' $ readFile fileName) :: IO (Either IOException String)
case result of
Left e -> putStrLn $ "發生 Exception:" ++ show e
Right contents -> putStrLn $ "檔案內容:" ++ contents
catches 函式
有時候會想捕捉多個 Exception,例如:
import Data.Typeable
import Control.Exception
import System.Environment
main =
(do (fileName:_) <- getArgs
contents <- readFile fileName
putStrLn contents)
`catch` (\(e :: IOException) -> putStrLn $ "發生 Exception:" ++ show e)
`catch` (\(e :: SomeException) -> putStrLn $ show $ typeOf e)
這個方式乍看行得通,不過有點問題,因為這是將前一個 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 Control.Exception
import System.Environment
main =
(do (fileName:_) <- getArgs
contents <- readFile fileName
putStrLn contents)
`catches` [
Handler (\(e::IOException) -> putStrLn $ "發生 Exception:" ++ show e),
Handler (\(e::SomeException) -> putStrLn $ show $ typeOf e)
]
handle 函式
handle 函式的型態為 Exception e => (e -> IO a) -> IO a -> IO a,與 catch 函式相比,只是引數順序不同:
import Control.Exception
import System.Environment
main = handle putStrLnIOException putStrLnContent
putStrLnContent :: IO ()
putStrLnContent = do
(fileName:_) <- getArgs
contents <- readFile fileName
putStrLn contents
putStrLnIOException :: IOException -> IO ()
putStrLnIOException e = do
putStrLn $ show e
可基於可讀性來選擇使用 catch 或 handle,通常在處理 Exception 函式簡短的情況下,可選擇使用 handle,例如:
import Control.Exception
import System.Environment
main = handle (\(e :: IOException) -> putStrLn $ "發生 Exception:" ++ show e)
(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 Control.Exception
import System.Environment
main =
(do (fileName:_) <- getArgs
contents <- readFile fileName
putStrLn contents)
`catches` [
Handler (\(e::IOException) -> putStrLn $ "發生 Exception:" ++ show e),
Handler (\(e::SomeException) -> putStrLn $ show $ typeOf e)
]
`finally` (putStrLn "finally 執行最後資源善後")
額外補充說明
因為這系列文件不談論 Haskell 的檔案輸入輸出,接下來只是額外補充,略過也沒關係…XD
finally 函式只是 Haskell 中 bracket 函式的特化,對於自行開檔與關檔這類動作時,使用 bracket 會比較方便,例如可使用 bracket 自行實作 readFile 函式:
import Data.Typeable
import Prelude hiding (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 (\(e::IOException) -> putStrLn $ "發生 Exception:" ++ show e),
Handler (\(e::SomeException) -> putStrLn $ show $ typeOf e)
]


