Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

exceptions and monad transformers

I'm using the EitherT monad transformer. Combining it with the IO monad, I'm afraid I would get an exception and it would not be caught.

Indeed the exception just passes through:

import Control.Monad.Trans
import Control.Error
import System.Directory

main = runEitherT testEx >>= print

testEx :: EitherT String IO ()
testEx = lift $ removeFile "non existing filename"

But the EitherT otherwise fits the bill perfectly to convey to callers the error. So I want to use that, not throw exceptions...

I looked at try from Control.Exception:

try :: Exception e => IO a -> IO (Either e a) 

It looks to be exactly what I want, it would fit in my EitherT IO stack... (probably with an added hoistEither and maybe fmapL and it starts looking verbose though) But a naive lift $ try doesn't typecheck.

I'm sure this problem has been solved thousands of times, but I can't find any good link describing this exact issue. How is this supposed to be solved?

EDIT By "how is this supposed to be solved", I was interested in the idiomatic solution, what would be the standard way to handle that in haskell. From the answers so far, it seems the idiomatic way is to let the exceptions be thown and handle them higher-up. Seems like a bit counter-intuitive to have two flows of control and return paths, but it is apparently the way it's meant to be done.

like image 288
Emmanuel Touzery Avatar asked Sep 09 '14 20:09

Emmanuel Touzery


3 Answers

I actually think EitherT is not the right thing to do here. What you're trying to say is "IO is for side-effects, and EitherT is for exceptions." But that's not true: IO always has the potential to result in an exception, so all you're doing is adding a false sense of security to your API, and introducing two ways that exceptions can be thrown instead of one. In addition, instead of using the well structured SomeException favored by IO, you're reducing down to String, which throws away information.

Anyway, if you're convinced that this is what you want to do, it's not too difficult. It looks something like:

eres <- liftIO $ try x
case eres of
    Left e -> throwError $ show (e :: SomeException)
    Right x -> return x

Note, however, that this will also swallow up async exceptions, which is usually not what you want to do. I think a better approach for that is enclosed-exceptions.

like image 106
Michael Snoyman Avatar answered Oct 21 '22 18:10

Michael Snoyman


You don't want to lift trying the computation, then you'd get an Exception e => EitherT a IO (Either e ()).

testEx :: (Exception e, MonadTrans m) => m IO (Either e ())
testEx = lift . try $ fails

You don't want the error in the result, you want to integrate the error into the EitherT. You want to integrate trying somethign with your EitherT

testEx :: (Exception e) => EitherT e IO ()
testEx = EitherT . try $ fails

We'll do this in general, then get just the message you want.

Integrate try with EitherT

You can extract the idea of integrating try with EitherT

tryIO :: (Exception e) => IO a -> EitherT e IO a
tryIO = EitherT . try

Or, for any underlying MonadIO as

tryIO :: (Exception e, MonadIO m) => IO a -> EitherT e m a
tryIO = EitherT . liftIO . try

(tryIO conflicts with a name from Control.Error. I couldn't come up with another name for this.)

You can then say you are willing to catch any exception. SomeException will catch all exceptions. If you are only interested in specific exceptions, use a different type. See Control.Exception for the details. If you aren't sure what you want to catch, you probably only want to catch IOExceptions; this is what tryIO from Control.Error does; see the last section.

anyException :: EitherT SomeException m a -> EitherT SomeException m a
anyException = id

You only want to keep the error message from the exception

message :: (Show e, Functor m) => EitherT e m a -> EitherT String m a
message = bimapEitherT show id

Then you can write

testEx :: EitherT String IO ()
testEx = message . anyException . tryIO $ fails

Integrate try with MonadError

You can instead integrate trying something with any MonadError, using MonadError and MonadIO to penetrate the transformer stack.

import Control.Monad.Except

tryIO :: (MonadError e m, MonadIO m, Exception e) => IO a -> m a
tryIO = (>>= either throwError return) . liftIO . try

You can write testEx in terms of this tryIO and anyException and message from the previous section

testEx :: EitherT String IO ()
testEx = message . anyException . tryIO $ fails

tryIO from Control.Error

The tryIO from Control.Error is essentially our first tryIO, except it only catches IOExceptions instead of any exception. It's actually defined as

tryIO :: (MonadIO m) => IO a -> EitherT IOException m a
tryIO = EitherT . liftIO . try

We can use it with message to write testEx as

testEx :: EitherT String IO ()
testEx = message . tryIO $ fails
like image 41
Cirdec Avatar answered Oct 21 '22 17:10

Cirdec


This is another simple approach: Let's define a custom monad transformer just like EitherT is defined:

{-# LANGUAGE FlexibleInstances, FunctionalDependencies #-}
import Control.Arrow (left)
import Control.Exception
import Control.Monad
import Control.Monad.Trans
import Control.Monad.Error
import Control.Monad.IO.Class

newtype ErrT a m b = ErrT { runErrT :: m (Either a b) }

instance (Monad m) => Monad (ErrT a m) where
    -- ...

instance (Monad m) => MonadError a (ErrT a m) where
    -- ...

instance MonadTrans (ErrT a) where
    lift = ErrT . liftM Right

together with the appropriate Applicative, Monad and MonadError instances.

Now let's add a means for an IOError to be converted to our error type. We can have a type class for this so that we're free in how we use the transformer.

class FromIOError e where
    fromIOException :: IOError -> e

Finally, we'll implement MonadIO in such a way that liftIO always catches IOErrors and converts them to the pure data type in the left part:

instance (MonadIO m, FromIOError a) => MonadIO (ErrT a m) where
    liftIO = ErrT . liftIO . liftM (left fromIOException)
                  . (try :: IO a -> IO (Either IOError a))

Now if we put all this in a module and export just the data type, runErrT, but not the constructor ErrT, everything that does IO within ErrT will have the exceptions properly handled, because IO actions can be introduced only through liftIO.

It'd be also possible to replace IOError with SomeException and handle all exceptions, if desired.

like image 1
Petr Avatar answered Oct 21 '22 17:10

Petr