nested Alternative (or MonadPlus)
最近仕事でattoparsecを使ってパーサを書いたりしている。パーサを書く際はAlternativeクラスの(<|>)が便利だ。
(<|>) :: f a -> f a -> f a
(a <|> b)
とはざっくり言って、「aを実行して失敗したらbを実行する」ということ。Alternativeをアクションだととらえれば、これは「アクションaが例外を投げたらそれを全てキャッチして無視し、bを実行する」という(やや乱暴な)例外処理として考えることもできる。
さて、Alternativeのインスタンスであるモナドはいろいろある。例えばMaybeがそうである。また一方で、Alternativeのインスタンスを作るモナドトランスフォーマーもある。例えばExceptTである。
そこで先日ふと気になったのだが、例えばモナド(ExceptT e Maybe)は(<|>)についてどのような挙動を取るのだろうか?この場合、outer monad(ExceptT)とinner monad(Maybe)の2種類の例外が存在する。普段何気なく(<|>)を使っているとこういう基本的なところが分からなくなる。
ということで、実験してみた。
import Control.Applicative ((<|>)) import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Except (ExceptT, throwE) type NestedM = ExceptT String Maybe outerFail :: String -> NestedM String outerFail = throwE innerFail :: NestedM String innerFail = lift Nothing ret :: String -> NestedM String ret = return
で、ghciで試す。
*Main> outerFail "outer_fail" <|> ret "fallback" ExceptT (Just (Right "fallback")) *Main> innerFail <|> ret "fallback" ExceptT Nothing *Main> outerFail "outer_fail1" <|> outerFail "outer_fail2" ExceptT (Just (Left "outer_fail1outer_fail2")) *Main> outerFail "outer_fail1" <|> innerFail <|> outerFail "outer_fail2" <|> ret "fallback" ExceptT Nothing
これより、以下のことが分かる。
- NestedMの(<|>)は、outer monad(ExceptT)の例外をキャッチして後続のアクションを実行する。
- NestedMの(<|>)は、inner monad(Maybe)の例外はキャッチできず、inner monadは即座に例外状態(Nothing)に落ちる。
もちろん、このへんの挙動はモナドトランスフォーマーの仕様次第だが、少なくともExceptTはこうなっているらしい。
ExceptTの実装を見れば分かるが、結局outer monad(ExceptT)のアクションはinner monad(Maybe)のコンテクストで実行される。よってinner monadが例外状態に落ちれば、outer monadに実装された例外処理はそもそも実行されない。よってouter monadで定義された(<|>)はinner monadの例外をキャッチできない。当たり前といえば当たり前のことなのだ。
(<|>)は便利なメソッドだが、モナドスタック中に複数のinstanceが存在する場合は混乱を招くかもしれない。それに対し、MonadThrowとMonadCatchを用いた例外処理(いわゆるJavaっぽい例外処理)は投げられた例外の型によって脱出スコープを制御できる。そのため、一つの例外処理モナド(例えばCatchT)でいろいろな脱出スコープを実現できる。その拡張性・柔軟性が、MonadThrow/MonadCatchの巧みな点かなと思った。