Haskellの例外処理

(2017.6.11) 新規作成。

概要

Haskell にも「例外」(exception) があります。Java や C#, C++ のような try-catch-finally (C++ には finally はありませんが.) といった専用の構文ではなく, 関数で取扱います。

これまでは, base パッケージの Control.Exception モジュールを使っていました。最近, これまでの例外の難点を解消した safe-exceptions パッケージが出ています。新しいコードでは, このパッケージを使うようにします。

型が IO に限定されず、非同期例外にも対応している。

# cabal install --global --dry-run safe-exceptions
Resolving dependencies...
In order, the following would be installed (use -v for more details):
transformers-compat-0.5.1.4
exceptions-0.8.3
safe-exceptions-0.1.4.0

優れた解説: Asynchronous Exception Handling in Haskell

Exceptions Best Practices in Haskell

try-catchみたいな

例外が発生したときに捕捉するには, catch関数を使います。Java, C++ などでの try-catch 構文に相当します.

例外型は, Exception 型クラスのインスタンスになります。IOException, SomeException など.

catch :: (MonadCatch m, Exception e) =>
         m a         -- 実行する本体
      -> (e -> m a)  -- 例外ハンドラ
      -> m a
  -- Defined in ‘Control.Exception.Safe’

現代では, 例外を送出 (throw) するかどうかは, IOモナドかどうか, ではありません。例えば, 純粋関数に見える head 関数も例外を投げます。なので, catchでも、本体の戻り値に IO を仮定しません。

catch は, 本体のなかで例外が発生すると, そこで実行を中断し, 例外ハンドラを呼び出す。例外が発生した場合の catch の戻り値は, ハンドラの戻り値。

catchIOIOException 型の例外を受けるための特殊版。

catchIO :: MonadCatch m => m a -> (IOException -> m a) -> m a

次の例は, 例外を投げて, 捕捉します。

Haskell
[RAW]
  1. {-# LANGUAGE ScopedTypeVariables #-} -- err::SomeException の部分
  2. import Control.Exception.Safe
  3. -- 自分の例外型
  4. data MyException = MyException String deriving (Show)
  5. instance Exception MyException
  6. main :: IO ()
  7. main = do
  8. catch (do
  9. print "start"
  10. -- throw, throwIO, throwM は同じ.
  11. _ <- throw $ MyException "my-exception!"
  12. print "finish" )
  13. (\ (err::SomeException) -> do
  14. print "caught"
  15. print err )

異なる例外型が上がってくる場合は, catchesを使います。

Haskell
[RAW]
  1. {-# LANGUAGE ScopedTypeVariables #-} -- err::SomeException の部分
  2. -- catches :: (MonadCatch m, MonadThrow m) => m a -> [Handler m a] -> m a
  3. import Control.Exception.Safe
  4. main :: IO ()
  5. main = do
  6. catches (do
  7. -- putStr $ show (div 100 0) -- 'divide by zero' error
  8. cs <- readFile "not-found"
  9. putStr cs )
  10. [ Handler $ \(err::IOException) -> (do print "file not found!"
  11. print err) ,
  12. Handler $ \(err::SomeException) -> (do print "unknown exception!"
  13. print err)
  14. ]
  15. print "finished"

try関数

try関数は, Either モナドで返します。

try :: (MonadCatch m, Exception e) => m a -> m (Either e a)
        -- Defined in ‘Control.Exception.Safe’

例外の形で失敗するかもしれない文を受けて, 分岐で判定します。失敗した (例外) 場合が Left です。

Haskell
[RAW]
  1. {-# LANGUAGE ScopedTypeVariables #-} -- err::SomeException の部分
  2. import Control.Exception.Safe
  3. main :: IO ()
  4. main = do
  5. result <- try $ readFile "not-found.txt"
  6. case result of
  7. Left (err::IOException) -> do { print "error!!"; print err }
  8. Right cs -> putStr cs

確実にリソースを解放: bracket関数

Haskell にもfinally 関数はあるが、大抵 finally を使いたいのはリソースの解放を確実にするため。このような場合には, 便利関数として bracket 関数が使える.

便利関数は複雑な状況への対応なども注意深く実装されているため、自分で似たコードを実装せず、積極的に利用しよう。

bracket 関数は, Control.Exception.Safe モジュール (safe-exceptions パッケージ) で定義されている.

bracket の型はこうなっている;

bracket :: MonadMask m => 
           m a          -- リソースハンドルを返す関数
        -> (a -> m b)   -- リソースを解放する関数. 必ず実行される
        -> (a -> m c)   -- 本体
        -> m c
    -- Defined in ‘Control.Exception.Safe’

ただし, MonadMask 型クラスは, Control.Monad.Catch.MonadMask です。

bracket の第3引数, 本体の部分で例外が発生した場合は、解放する関数を実行したうえで、例外が上がる.

次は, bracketの使用例。hIsEOF で検査せず, わざと例外が発生するようにしている。ハンドルが閉じられてから、例外が再送出される。

Haskell
[RAW]
  1. -- 今どきは, .Safe を付けること.
  2. import Control.Exception.Safe
  3. import System.IO
  4. main :: IO ()
  5. main = do
  6. bracket (openFile "bracket.hs" ReadMode)
  7. (\fp -> putStrLn "<<Closing file>>" >> hClose fp)
  8. loop
  9. where
  10. loop fp = do
  11. --eof <- hIsEOF fp
  12. --if eof then return ()
  13. -- else do
  14. line <- hGetLine fp -- ここで例外発生: end of file
  15. putStrLn line
  16. loop fp

せっかくなので、bracket の定義を見ておこう. mask関数で, 非同期例外をいったん保留にしている。

Haskell
[RAW]
  1. bracket before after thing = C.mask $ \restore -> do
  2. x <- before
  3. res1 <- C.try $ restore (thing x)
  4. case res1 of
  5. Left (e1 :: SomeException) -> do
  6. -- explicitly ignore exceptions from after. We know that
  7. -- no async exceptions were thrown there, so therefore
  8. -- the stronger exception must come from thing
  9. --
  10. -- https://github.com/fpco/safe-exceptions/issues/2
  11. _ :: Either SomeException b <-
  12. C.try $ C.uninterruptibleMask_ $ after x
  13. C.throwM e1
  14. Right y -> do
  15. _ <- C.uninterruptibleMask_ $ after x
  16. return y

昔の The Haskell 98 (Revised) Report では、次の定義になっていました。IOモナドを仮定し、非同期例外への対応もありませんでした。

Haskell
[RAW]
  1. bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
  2. bracket before after m = do
  3. x <- before
  4. rs <- try (m x)
  5. after x
  6. case rs of
  7. Right r -> return r
  8. Left e -> ioError e

mask 関数の型はこう;

Control.Monad.Catch.mask
  :: MonadMask m => ((forall a. m a -> m a) -> m b) -> m b