Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 477
Elliot Cameron Avatar asked Nov 02 '16 03:11

Elliot Cameron


2 Answers

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
like image 121
danidiaz Avatar answered Oct 18 '22 20:10

danidiaz


My recommendation: have your code live in IO instead of ExceptT, and wrap each handler function in a ExceptT . try.

like image 33
Michael Snoyman Avatar answered Oct 18 '22 20:10

Michael Snoyman