拋出與自訂例外

February 11, 2022

Haskell 有多種處理錯誤的方式,像是〈8 ways to report errors in Haskell〉就提出了八種方式,這邊先從簡單的開始。

error 與 ioError

自行拋出 Exception 最簡單的方式是使用 error 函式,它的型態是 GHC.Stack.Types.HasCallStack => [Char] -> a,接受一個字串並拋出 Exception,例如〈Data.List/Set/Map 模組〉就使用過 error 來自訂 initlast 函式:

init' :: [a] -> [a]
init' [] = error "empty list"
init' (x:[]) = []
init' (x:xs) = x : init' xs

last' :: [a] -> a
last' [] = error "empty list"
last' (x:[]) = x
last' (_:xs) = last' xs

在〈try/catch/finally〉談過,從純綷的函式中拋出 Exception,並不是要讓你在執行時期嘗試處理,執行純函式時若發生了 Exception,往往表示呼叫函式的條件不足而發生的錯誤,你應該停止程式,檢視、修改程式碼,做好呼叫函式前的條件檢,別讓函式有機會拋出 Exception

若是個有副作用的函式,要引發例外可以使用 ioError 函式,它的型態是 IOError -> IO a,例如:

import Control.Exception
import System.Environment
import System.Directory
                               
main = putStrLnContent `catch` putStrLnIOException

putStrLnContent :: IO ()
putStrLnContent = do
    (fileName:_) <- getArgs
    isExist <- doesFileExist fileName
    if isExist then 
        do
            contents <- readFile fileName
            putStrLn contents
    else
        ioError $ userError $ fileName ++ "不存在"

putStrLnIOException :: IOException -> IO ()
putStrLnIOException e = do
    putStrLn $ show e

IOErrorIOException 的別名,由於沒有導出 IOException 的建構式,若要建立實例,可以使用 userError 函式,它的型態是 String -> IOError,也就是接受一個字串,傳回 IOError(而不是拋出)。

自訂 Exception 實例

Exception 是個型態類別,宣告為 class (Typeable e, Show e) => Exception e,具有 toException :: e -> SomeExceptionfromException :: SomeException -> Maybe e 兩個行為,可用來自訂 Exception 階層,已經有預設實作,如果想知道如何自訂 Exception 階層,可參考〈The Exception type〉。

因為 Exception 宣告時,限定型態必須為 TypeableShow 的實例,因此,自訂 Exception 實例時,型態也必須衍生自 TypeableShow。例如以下是個自訂 Exception 的實例:

import Data.Typeable
import Control.Exception

data ListException = EmptyListException String deriving (Show, Typeable)

instance Exception ListException

init' :: [a] ->  [a]
init' [] = throw $ EmptyListException "an empty list has no init"
init' (x:[]) = []
init' (x:xs) = x : init' xs

throw 與 throwIO

在上例中看到了 throw,它的型態是 Exception e => e -> a,可用來將 Exception 拋出。

throw 的兄弟之一是 throwIO,型態是 Exception e => e -> IO a,因此跟 ioError 一樣,使用了這個函式的地方必然就不是純綷、無副作用的函式,不同的是, ioError 只接受 IOError,而 throwIO 可接受任何 Exception,因此指定自訂的 Exception 實例。例如:

import System.IO
import Data.Typeable
import Control.Exception

data ArithmeticException = DivisionByZero String deriving (Show, Typeable)

instance Exception ArithmeticException

answer :: (Eq a, Show a, Fractional a) => a -> a -> IO ()
answer a b =
    if b /= 0 then do 
        putStrLn $ show (a / b)
    else
        throwIO $ DivisionByZero $ (show a) ++ "/" ++ (show b)
                 
main = do
    putStr "a:"
    hFlush stdout
    a <- getLine
    putStr "b:"
    hFlush stdout
    b <- getLine
    answer (read a) (read b)

這個範例可以輸入 a、b 兩個數字,回答 a / b 的結果,如果 b 為零會拋出例外:

>divide        
a:10
b:20
答案:0.5

>divide
a:10
b:0
divide: DivisionByZero "10.0/0.0"

有了這幾篇介紹 Exception 的基礎,如果想要瞭解更多 Exception 的處理,建議參考 Haskell 官方的 Control.Exception

分享到 LinkedIn 分享到 Facebook 分享到 Twitter