Question

Short version

Same question as in here, but within a generic MonadResource instance rather than an explicit ResourceT m.

Long version

How would you define a catch function such that:

import Control.Exception            (Exception, IOException)
import Control.Monad.Trans.Resource (MonadResource, runResourceT)

catch :: (MonadResource m, Exception e) -> m () -> (e -> m ()) -> m ()
catch = undefined

-- 'a' and 'b' are functions from an external library,
-- so I can't actually change their implementation
a, b :: MonadResource m => m ()
a = -- Something that might throw IO exceptions
b = -- Something that might throw IO exceptions

main :: IO ()
main = runResourceT $ do
    a `catch` \(e :: IOException) -> -- Exception handling
    b `catch` \(e :: IOException) -> -- Exception handling

The problems I run into are:

It looks like the only way to handle IO exceptions is to exit the ResourceT layer, and this bothers me: I'd like to be able to handle exceptions locally without travelling through the monad transformers stack.

For information, in my real code, a and b are actually the http function from Network.HTTP.Conduit.

Thank you for your insights.

Minimal code with the problem

Compilable with ghc --make example.hs with http-conduit library installed:

{-# LANGUAGE FlexibleContexts, ScopedTypeVariables #-}
import Control.Exception.Lifted     (IOException, catch)
import Control.Monad.Base           (liftBase)
import Control.Monad.Error          (MonadError(..), runErrorT)
import Control.Monad.Trans.Control  (MonadBaseControl)
import Control.Monad.Trans.Resource (MonadResource, runResourceT)

import Data.Conduit
import Data.Conduit.List            (consume)
import Data.Conduit.Text            (decode, utf8)
import Data.Text                    (Text)

import Network.HTTP.Client
import Network.HTTP.Conduit         (http)

main :: IO ()
main = do
    result <- runErrorT $ runResourceT f
    putStrLn $ "OK: " ++ show result

f :: (MonadBaseControl IO m, MonadResource m, MonadError String m) => m [Text]
f = do
    req      <- liftBase $ parseUrl "http://uri-that-does-not-exist.abc"
    manager  <- liftBase $ newManager defaultManagerSettings
    response <- (http req manager `catch` \(e :: IOException) -> throwError $ show e)
    response $$+- decode utf8 =$ consume

When executed, this program ends in error with the following output:

InternalIOException getAddrInfo: does not exist (Name or service not known)
Was it helpful?

Solution

http does not throw IOException, it throws HttpException and InternalIOException is one of the latter's constructors.

You should either catch HttpException or SomeException in case you want to catch all exceptions.

OTHER TIPS

The type you need,

a, b :: MonadResource m, MonadBaseControl IO m => m ()

Is a special case of the type you currently have

a, b :: MonadResource m => m ()

as the only difference is the extra class constraint. You are free to make the type signatures in your code less general than they would be by default; therefore, changing the signatures of a and b should be enough.

If I understand your problem correctly, there is no problem with using lifted-base. Although the type of a and b only use the constraint MonadResource m, it doesn't mean you can't use them on a monad that has other additional properties. For example, if you perform your computation inside ResourceT, it satisfies the constraint for a and b, and you can also use anything from Control.Exception.Lifted:

-- ...
import Control.Exception.Lifted

-- 'a' and 'b' are functions from an external library,
-- so I can't actually change their implementation
a, b :: MonadResource m => m ()
a = undefined -- Something that might throw IO exceptions
b = undefined -- Something that might throw IO exceptions

main :: IO ()
main = runResourceT $ do
    a `catch` \(e :: IOException) -> undefined -- Exception handling
    b `catch` \(e :: IOException) -> undefined -- Exception handling

If you alter the type signature of catch to include MonadCatch from the exceptions package, then it would be trivial:

import Control.Monad.Trans.Resource (MonadResource, runResourceT)
import Control.Monad.Catch          (catch)

a, b :: MonadResource m => m ()
a = …
b = …

main :: IO ()
main = runResourceT $ do
    a `catch` \e -> …
    b `catch` \e -> …

Note that this does not require any changes for a nor b.

Also, both duplode and Petr Pudlák have pointed out that you are free to make the monad for catch to be as specific as you like, because doing so does not require any cooperation from a or b. So any of these solutions will work.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top