Reputation:
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
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
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