Instance MonadPlus IO
is unique because mzero
throws:
Prelude Control.Monad> mzero
*** Exception: user error (mzero)
So accordingly, MonadPlus IO
implies that it is also intended for errors.
mzero
apparently serves as the identity element if the other action doesn't throw:
Prelude Control.Monad> mzero `mplus` return 0
0
Prelude Control.Monad> return 0 `mplus` mzero
0
But it doesn't when both actions throw:
Prelude Control.Monad> fail "Hello, world!" `mplus` mzero
*** Exception: user error (mzero)
Prelude Control.Monad> mzero `mplus` fail "Hello, world!"
*** Exception: user error (Hello, world!)
So MonadPlus IO
is not a monoid.
If it violates MonadPlus
laws when user intends errors, what is it actually intended for?
IO
under mplus
is a monoid relative to an equivalence class that identifies exceptions. Not that satisfying. An alternative approach might look like this:
m <|> n = m `catches`
[ Handler $ \ ~EmptyIO -> n
, Handler $ \ ~se@(SomeException _) ->
n `catch` \ ~EmptyIO -> throwIO se ]
The main problem with this approach is that handlers can stack up. When the first action fails, we can't just commit to the second action. A smaller issue is that there's no completely reliable way to determine whether an exception is synchronous (and should be rethrown using throwIO
) or asynchronous (in which case we need to rethrow it using throwTo
with our own thread ID). So that way lies messes.