haskellでの例外について(Parallel and Concurrent Programming in Haskell Chapter 8)

例外について(Parallel and Concurrent Programming in Haskell Chapter 8)

英語の原文はこちらのページで読める
http://chimera.labs.oreilly.com/books/1230000000929/ch08.html#sec_exceptions

前後に非同期処理の話があるがそれは別にまとめることにする

haskellでは例外に関して特別な構文などを言語レベルで用意してはいない
他の関数と同じく、例外についてもライブラリに含まれる関数の一つである
そのおかげで高階関数などを用いた抽象化を例外にも施すことができる

例外を投げる関数は以下

throw :: Exception e => e -> a

Exception型クラスのinstanceになっている型をとり、制約なく任意の型を返す
任意の型を返すということはどこでも使えるということ

Exception型クラスのinstanceとするにはTypeableとShowのinstanceであればよい
具体例はErrorCallでこのようになっている

newtype ErrorCall = ErrorCall String deriving (Typeable)

instance Show ErrorCall where { ... }
instance Exception ErrorCall

よく使われるerrorという関数はErrorCallを用いて以下のように定義されている

error :: String -> a
error s = throw (ErrorCall s)

IOの中ではSystem.IO.Errorで定義されたIOExceptionが使われる
IOの中で例外をキャッチするにはcatchを使う

catch :: Exception e => IO a -> (e -> IO a) -> IO a

実行したいIOアクションと例外が起こった際のハンドラーをとってIOを返す
どの例外を捕捉するかはハンドラーに渡す引数eで決まる

ghciでやってみる

$ ghci
GHCi, version 7.8.3: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Prelude> import Control.Exception
Prelude Control.Exception> import Data.Typeable
Prelude Control.Exception Data.Typeable> :set -XDeriveDataTypeable
Prelude Control.Exception Data.Typeable> data MyEx = MyEx deriving (Show, Typeable)
Prelude Control.Exception Data.Typeable> instance Exception MyEx
Prelude Control.Exception Data.Typeable> throw MyEx
*** Exception: MyEx

自作の例外を作成してthrowした結果
XDeriveDataTypeableはTypeableを自動で導出するために必要なオプションである

catchしてみる

Prelude Control.Exception Data.Typeable> throw MyEx `catch` \e -> print e

<interactive>:8:12:
    No instance for (Exception a0) arising from a use of ‘catch’
    The type variable ‘a0’ is ambiguous
    Note: there are several potential instances:
      instance Exception MyEx -- Defined at <interactive>:6:10
      instance Exception ArithException -- Defined in ‘GHC.Exception’
      instance Exception ErrorCall -- Defined in ‘GHC.Exception’
      ...plus 8 others
    In the expression: throw MyEx `catch` \ e -> print e
    In an equation for ‘it’: it = throw MyEx `catch` \ e -> print e

エラーになった。これはハンドラーに渡す例外を一意に決めていないためである
なので以下のようにeの型を指定する

Prelude Control.Exception Data.Typeable> throw MyEx `catch` \e -> print (e :: MyEx)
MyEx
Prelude Control.Exception Data.Typeable> throw MyEx `catch` \e -> print (e :: SomeException)
MyEx
Prelude Control.Exception Data.Typeable> throw MyEx `catch` \e -> print (e :: ArithException)
*** Exception: MyEx

e :: MyExとすればそのままキャッチできる
SomeExceptionはすべての例外をキャッチできる型であり、これでもMyExがキャッチできた
ArithExceptionを指定した場合はMyExはスルーされて例外が表示されている

SomeExceptionの使いどころはデバッグ時や例外を一度受けて再送出したい場合など

例外処理をするためにいくつの関数が用意されている

try :: Exception e => IO a -> IO (Either e a)

tryは一つ目に指定したIOで例外が起きるとLeftの値として返してくれる

onException :: IO a -> IO b -> IO a

onExceptionは一つ目のIOで例外が起きた場合に2つ目のアクションを実行して再度例外を投げなおす

throwIO :: Exception e => e -> IO a

throwのIO内で使うバージョン
throwとの違いは実行順序がプログラムの通りとなることである
throwの場合はその他のアクションとの実行順序が入れ替わる可能性があるため、IO内ではthrowIOを使うべきである

bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c

bracketはリソースの取得解放によく使う
一つ目のIOでリソースの取得など初期化処理を行う
その返り値が2つ目、3つ目のIOの引数aとなっている
2つ目のIOは3つ目のIO実行後または例外発生時に実行されるため、ここでリソースの解放処理を行う
3つ目が実際に行いたい処理自体を書くところである

ファイルのopen, closeがわかりやすい
おそらくwithFileはこのようなことをやっているのだと思われる

bracket
  (openFile "hoge")
  (\h -> hClose h)
  (\h -> writeFile h WriteMode $ ...)

ちなみにbracketの単純な形はここまでに出てきた関数で定義できる

bracket before after during = do
  a <- before
  c <- during a `onException` after a
  after a
  return c

bracketの特殊バージョンで初期化処理は要らないが最後に特定の処理を必ず入れたい場合

finally :: IO a -> IO b -> IO a
finally io after = do
  io `onException` after
  after

Haskellによる並列・並行プログラミング

Haskellによる並列・並行プログラミング