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?
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
My recommendation: have your code live in IO instead of ExceptT, and wrap each handler function in a ExceptT . try
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With