Haskellの最近の例外ハンドリング

どうもHaskellには標準のControl.Exceptionモジュールだけでなくmtlexceptionsexceptionalといった例外を扱うためのパッケージがあるらしいのだが、そのあたりのパッケージの選び方や使い方についてまとまった情報を見つけられなかった。 HaskellWiki例外のページも少々古いようで、deprecatedなものや統合される前のパッケージを書いていたりする。

調べた限り、mtlexceptionsが今の主流っぽい。 その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!"

throwErrorcatchErrorMonadErrorのプリミティブで、 runExceptTExceptTをはがす役である。

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)

実はIOMonadErrorインスタンスなので、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)

ただし、IOMonadErrorとして使う場合は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との違いはthrowthrowMに変わるくらいである。 Control.ExceptionthrowモナドIOに限定されてしまうが、exceptionsthrowMではMonadThrowを実装しているモナドであればよい。 MonadThrowの実装が提供されているのはIOのほかにはmtlパッケージの各モナド(WriterTなど)がある。

まとめ

例外のためにモナドスタックを一段増やすのは例外の意義から言ってもやりすぎのような気がする。 IOがすでに失敗しうるという意味を含んでいるし。

個人的には実用上はexceptionsを使えばよいと思う。