[Haskell-cafe] MonadCatchIO, finally and the error monad

Michael Snoyman michael at snoyman.com
Thu Oct 14 06:01:59 EDT 2010


Hey all,

In case anyone noticed, Haskellers occassionally dies with a "Pool
exhausted exception." I've traced this to a bug in Yesod, which in
turn is a bug in the neither package, which I believe is a flawed
design in the MonadCatchIO-transformers package. Here are my thoughts
on this and what I think needs to be done to fix it.

In Control.Exception, we define a number of different ways of dealing
with exceptions. All of these can be expressed in terms of block,
unblock and catch. For our purposes here, I'm going to ignore block
and unblock: they deal with asynchronous exceptions, which is not my
point here. Keep that in mind with the code samples. Anyway, with this
caveat, we can define finally as:

finally :: IO a -> IO b -> IO a
a `finally` sequel = do
    r <- a `catch` \e -> sequel >> throwIO (e :: SomeException)
    _ <- sequel
    return r

The idea is simple: try to perform the action. If any exceptions get
thrown, call sequel and rethrow the exception. If we ever get to line
4, it's because no exceptions were thrown. Therefore, we know that
sequel has not yet been called, so we call it. Said another way: there
are precisely two cases:

* An exception was thrown
* An exception was not thrown

A downside of this finally function (and catch, for that matter) is
that it requires all of the actions to live in the IO monad, when in
fact we all love to let things run in complicated monad transformer
stacks. So along comes MonadCatchIO-(transformers, mtl) and gives us a
new magical definition of catch:

catch :: (MonadCatchIO m, Exception e) => m a -> (e -> m a) -> m a

Using this new, extended definition of catch, we can define a finally
function with the type signature


finally :: MonadCatchIO m => m a -> m b -> m a

(Note that we need to replace throwIO with liftIO . throwIO.) You can
try this with writers, readers, etc, and everything works just fine.
You can even use an Error/Either monad transformer, throw an
exception, and the finally function will correctly run your sequel
function.

However, things don't work out so well when you use a throwError.
Let's see the code:

{-# LANGUAGE PackageImports #-}
import Control.Monad.Trans.Error
import "MonadCatchIO-transformers" Control.Monad.CatchIO (finally)
import Control.Monad.IO.Class

main = runErrorT $ finally go $ liftIO $ putStrLn "sequel called"

go :: ErrorT String IO String
--go = return "return"
--go = error "error"
--go = throwError "throwError"

Try running the code with each version of go uncommented. In the first
two, "sequel called" gets printed. However, in the third, it does not.
The reason is short-circuiting: if we remember from the definition of
finally, there are two cases we account for. If an exception is
called, catch addresses it. If not, we assume that the next line will
be called. However, in the presence of short-circuiting monads like
ErrorT, that line of code will never get called!

I have a recommendation of how to fix this: the MonadCatchIO typeclass
should be extended to include finally, onException and everything
else. We can provide default definitions which will work for most
monads, and short-circuiting monads like ErrorT (and I imagine ContT
as well) will need to override them.

Michael


More information about the Haskell-Cafe mailing list