Haskellの最近の例外ハンドリング
どうもHaskellには標準のControl.Exception
モジュールだけでなくmtl
やexceptions
やexceptional
といった例外を扱うためのパッケージがあるらしいのだが、そのあたりのパッケージの選び方や使い方についてまとまった情報を見つけられなかった。
HaskellWiki例外のページも少々古いようで、deprecatedなものや統合される前のパッケージを書いていたりする。
調べた限り、mtl
とexceptions
が今の主流っぽい。
その2つの使い方をまとめる。
なおバージョンはlts-6.1を基準としている。
mtl
mtl
パッケージのControl.Monad.Except
モジュールはMonadError
というモナドとExceptT
というモナド変換子を提供する。
以下のように使う。
import Control.Monad.Trans(lift) import Control.Monad.Except(runExceptT, throwError, catchError) main :: IO () main = do runExceptT $ do (do lift $ print "start" throwError "exception!" lift $ print "finish" ) `catchError` (\e -> do lift $ print "caught" lift $ print e ) return () -- > "start" -- > "caught" -- > "exception!"
throwError
とcatchError
がMonadError
のプリミティブで、
runExceptT
はExceptT
をはがす役である。
catchError
でキャッチしない場合はEither
として取り出せる。
main = do ret <- runExceptT $ do lift $ print "start" throwError "exception!" return "ret value" print ret -- > "start" -- > Left "exception!"
あくまでthrowError
をキャッチするものだということに注意:
main = do ret <- runExceptT $ do lift $ print "start" file <- lift $ readFile "nothing.txt" throwError "exception!" lift $ print file return "ret value" print ret -- > "start" -- > except-test-exe: nothing.txt: openFile: does not exist (No such file or directory)
実はIO
もMonadError
のインスタンスなので、IO
中のIOException
をキャッチしたい場合はrunExceptT
なしで]
以下のように書ける。
main = do (do print "start" file <- readFile "nothing.txt" print file ) `catchError` (\e -> do print "caught" print e ) -- > "start" -- > "caught" -- > nothing.txt: openFile: does not exist (No such file or directory)
ただし、IO
をMonadError
として使う場合はthrowError
できるのはIOException
だけである。
以下のようなものはコンパイルエラーになってしまう。
main = do (do print "start" file <- readFile "nothing.txt" throwError "my error" -- > Couldn't match type ‘GHC.IO.Exception.IOException’ with ‘[Char]’ print file ) `catchError` (\e -> do print "caught" print e )
自作の別の例外を投げたいときはやはりrunExceptT
を使う必要がある。
exceptions
exceptions
パッケージのControl.Monad.Catch
モジュールは標準のControl.Exception
に対してよりフレンドリーである。
Control.Exception
の大部分をラップあるいは再エクスポートしており、Control.Exception
をインポートしなくても
Control.Exception
と同じ名前の関数が同じように使える。
{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE ScopedTypeVariables #-} import Data.Typeable import Control.Monad.Catch data MyException = MyException String deriving(Show, Typeable) instance Exception MyException data MyAnotherException = MyAnotherException String deriving(Show, Typeable) instance Exception MyAnotherException main = do (do print "start" throwM $ MyException "exception!" throwM $ MyAnotherException "another exception!" print "finish" ) `catch` (\(e::SomeException) -> do print "caught" print e ) -- > "start" -- > "caught" -- > MyException "exception!"
Control.Exception
との違いはthrow
がthrowM
に変わるくらいである。
Control.Exception
のthrow
はモナドがIO
に限定されてしまうが、exceptions
のthrowM
ではMonadThrow
を実装しているモナドであればよい。
MonadThrow
の実装が提供されているのはIO
のほかにはmtl
パッケージの各モナド(WriterT
など)がある。
まとめ
例外のためにモナドスタックを一段増やすのは例外の意義から言ってもやりすぎのような気がする。
IO
がすでに失敗しうるという意味を含んでいるし。
個人的には実用上はexceptions
を使えばよいと思う。