続・Haskellの最近の例外ハンドリング
前回 の記事ではHaskellの例外ハンドリングには exceptions パッケージを使えばいいのではないかと書いた。
ところが今年の6月に safe-exceptions という exceptions を拡張したようなパッケージがさる FPComplete から 発表 された。
そこでこの記事では safe-exceptions について調べてみる。 おそらくほぼ FPComplete の発表の受け売りになってしまうので英語を読める人は原文を読む方がいいかもしれない。
さすが FPComplete だけあってこれは既に LTS Haskell に入っている。 この記事では lts-6.14 を用いる。
Haskellの例外のつらいところ
自分が認識している範囲ではHaskellの例外まわりは以下のところがつらい。
- 標準の例外系の関数が
IO
に特化されていて取り回しが悪い - いかにも純粋そうな関数が例外を出したりスレッドセーフでなかったりする
- 例外型は何でも非同期例外として投げることができるし、キャッチ部で非同期例外かそうでないかの区別をつけられない
1.については前回の記事で紹介した exceptions パッケージによってかなり改善される。
2.は内部的にFFIでC関数を呼ぶライブラリを使うとたまにある。 まあ例外出すかやスレッドセーフかどうかはHaddockに大体書いてあるし、気になるのは好みの問題かもしれない。
3.が結構な問題である。
非同期例外というのは別のスレッドから来る例外で、
コード上ではtry節の外で発生する。
以下の someFunc
の catch
はもちろん InternalException
をキャッチするために書かれている。
しかし実際は ExternalException
をキャッチしてしまうのである!
{-# LANGUAGE ScopedTypeVariables, DeriveDataTypeable #-} module Main where import Data.Typeable (Typeable) import Control.Concurrent (forkIO, threadDelay) import Control.Exception data InternalException = InternalException String deriving (Show, Typeable) instance Exception InternalException data ExternalException = ExternalException String deriving (Show, Typeable) instance Exception ExternalException someFunc :: IO () someFunc = do ( do threadDelay $ 1000 * 1000 throw $ InternalException "error form inside!" ) `catch` (\(e::SomeException) -> do print e ) main :: IO () main = do threadId <- forkIO someFunc threadDelay $ 500 * 1000 throwTo threadId $ ExternalException "error from outside!" threadDelay $ 100 * 1000 return () -- > ExternalException "error from outside!"
この例では InternalException
と ExternalException
で同期例外と非同期例外を型で区別できそうにも見える。
しかし throwTo
はどんな例外でも投げることができるので InternalException
も投げる可能性を排除できない。
すべての catch
は非同期例外を受け取りうるし、キャッチした例外が同期か非同期か区別できないということを
念頭に置いて書かなければならない。
これは非常につらい。
safe-exceptions以前の対処
enclosed-exceptions パッケージというものがある。 enclosedの名前のとおり、try部の中の例外だけをキャッチする関数を提供する。
その仕組みは FPCompleteの記事 に書かれているが、 非同期処理をもって非同期例外を倒すという感じで面白い。
safe-exceptions
enclosed-exceptions は内部的に非同期処理を用いて非同期例外をうまく扱う。 しかし非同期処理を使うことのオーバーヘッドが生まれてしまった。
safe-exceptions は別のアプローチを用いる。
このライブラリは throw
や throwTo
をラップして同名で再エクスポートする。
これらは実際に例外を投げる「前」に例外を SyncExceptionWrapper
もしくは AsyncExceptionWrapper
で包んで投げる。
catch
は同期例外と非同期例外を型で区別できないので、投げるときに区別できるようにしてしまうわけだ。
非同期処理を使っていないので非同期処理によるオーバーヘッドもない。
このライブラリは throw
や throwTo
のほかにも、 throwIO
や catch
や catchIO
などの
おなじみの関数を再実装しているので新たに使いかたを学ぶ必要はほとんどない。
しかも exceptions と同様にモナド変換子に対応している。
もし危険性を理解して非同期例外もキャッチしたいときには catchDeep
が使える。
まとめ
safe-exceptions を使おう。