Search code examples
haskellexceptionresourcesmonad-transformersio-monad

What is the best way to manage resources in a monad stack like ExceptT a IO?


For better or for worse, Haskell's popular Servant library has made it common-place to run code in a monad transformer stack involving ExceptT err IO. Servant's own handler monad is ExceptT ServantErr IO. As many argue, this is a somewhat troublesome monad to work in since there are multiple ways for failure to unroll: 1) via normal exceptions from IO at the base, or 2) by returning Left.

As Ed Kmett's exceptions library helpfully clarifies:

Continuation-based monads, and stacks such as ErrorT e IO which provide for multiple failure modes, are invalid instances of this [MonadMask] class.

This is very inconvenient since MonadMask gives us access the helpful [polymorphic version of] bracket function for doing resource management (not leaking resources due to an exception, etc.). But in Servant's Handler monad we can't use it.

I'm not very familiar with it, but some people say that the solution is to use monad-control and it's many partner libraries like lifted-base and lifted-async to give your monad access to resource management tools like bracket (presumably this works for ExceptT err IO and friends as well?).

However, it seems that monad-control is losing favor in the community, yet I can't tell what the alternative would be. Even Snoyman's recent safe-exceptions library uses Kmett's exceptions library and avoids monad-control.

Can someone clarify the current story for people like me who are trying to plow our way into serious Haskell usage?


Solution

  • You could work in IO, return a value of type IO (Either ServantErr r) at the end and wrap it in ExceptT to make it fit the handler type. This would let you use bracket normally in IO. One problem with this approach is that you lose the "automatic error management" that ExceptT provides. That is, if you fail in the middle of the handler you'll have to perform an explicit pattern match on the Either and things like that.


    The above is basically reimplementing the MonadTransControl instance for ExceptT, which is

    instance MonadTransControl (ExceptT e) where
        type StT (ExceptT e) a = Either e a
        liftWith f = ExceptT $ liftM return $ f $ runExceptT
        restoreT = ExceptT
    

    monad-control works fine when lifting functions like bracket, but it has odd corner cases with functions like the following (taken from this blog post):

    import Control.Monad.Trans.Control
    
    callTwice :: IO a -> IO a
    callTwice action = action >> action
    
    callTwice' :: ExceptT () IO () -> ExceptT () IO ()
    callTwice' = liftBaseOp_ callTwice
    

    If we pass to callTwice' an action that prints something and fails immediately after

    main :: IO ()
    main = do
        let printAndFail = lift (putStrLn "foo") >> throwE ()
        runExceptT (callTwice' printAndFail) >>= print  
    

    It prints "foo" two times anyway, even if our intuition says that it should stop after the first execution of the action fails.


    An alternative approach is to use the resourcet library and work in a ExceptT ServantErr (ResourceT IO) r monad. You would need to use resourcet functions like allocate instead of bracket, and adapt the monad at the end like:

    import Control.Monad.Trans.Resource
    import Control.Monad.Trans.Except
    
    adapt :: ExceptT ServantErr (ResourceT IO) r -> ExceptT err IO r 
    adapt = ExceptT . runResourceT . runExceptT
    

    or like:

    import Control.Monad.Morph
    
    adapt' :: ExceptT err (ResourceT IO) r -> ExceptT err IO r 
    adapt' = hoist runResourceT