user1002430
user1002430

Reputation:

How to use catch inside an ErrorT in Haskell?

Let's say that I'd like to rework the following function:

runCmd :: FilePath -> ErrorT String IO ()
runCmd cmd = do
  Left e <- liftIO $ tryIOError $ do
    (inp, outp, errp, pid) <- runInteractiveProcess cmd [] Nothing Nothing
    mapM_ (`hSetBuffering` LineBuffering) [inp, outp, errp]
    forever (hGetLine outp >>= putStrLn) -- IO error when process ends
  unless (isEOFError e) $ throwError $ "IO Error: " ++ ioeGetErrorString e

It tries to run cmd and read the output. If it fails with an IO error, I catch it with tryIOError, pass it through to the enclosing ErrorT monad, and then deal with the error.

It's kind of a roundabout way to do it, especially since there are functions like catch and handle that allow me to use a handler to deal with the error. But they are typed IO:

handle :: Exception e => (e -> IO a) -> IO a -> IO a
catch  :: Exception e => IO a -> (e -> IO a) -> IO a

How do I cleanly restructure the above code so that I can handle the IO errors and pass it through the ErrorT monad?

Upvotes: 1

Views: 305

Answers (2)

daniel gratzer
daniel gratzer

Reputation: 53881

If you really want to use ErrorT, you can try something like this

import Control.Exception
import Control.Monad.Error

wrapException :: IO a -> ErrorT String IO a
wrapException io = do
  either <- liftIO $ tryJust Just io
  case either of
    Left e  -> throwError . show $ (e :: SomeException)
    Right v -> return v

But this isn't perfect because you still are limited to IO in what can throw exceptions. What you can do to help is to use

catchException :: ErrorT String IO a -> ErrorT String IO a
catchException = either throwError return <=< wrapException . runErrorT

What this does is grab any exceptions that are propagating up and trap them back in the ErrorT monad. This still isn't perfect since you need to explicitly wrap all exception throwing pieces of code in it. But it's not terrible.

Upvotes: 1

Benjamin Barenblat
Benjamin Barenblat

Reputation: 1311

I would avoid using an ErrorT for this in the first place. Have runCmd simply return an IO () (or throw an IOError if issues arise), and defer error handling to the callers:

runCmd :: FilePath -> IO ()
runCmd cmd =
  handleIOError (\e -> if isEOFError e then return () else ioError e) $ do
    -- same code as before
    (inp, outp, errp, pid) <- runInteractiveProcess cmd [] Nothing Nothing
    mapM_ (`hSetBuffering` LineBuffering) [inp, outp, errp]
    forever (hGetLine outp >>= putStrLn)
  where handleIOError = flip catchIOError

caller :: IO ()
caller = catchIOError (runCmd "/bin/ls") $ \e ->
  -- error handling code

If you need to catch the IOError in ErrorT code elsewhere, you can use liftIO there:

errorTCaller :: ErrorT String IO ()
errorTCaller = liftIO caller

Upvotes: 1

Related Questions