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)
]