Some weeks ago, I wrote an article about dealing with exceptions in Haskell. Shortly after that a friendly poster on Stack Overflow provided a better solution that I have been practicing with since. But, the solution requires some leaps of faith that I will here help you bridge.
Go back to my original article and read, in particular, the definition of
verifyFibFile. Once you have done that, take a look at this new version of the function:
catchError' :: ErrorT e IO a -> (IOError -> ErrorT e IO a) -> ErrorT e IO a catchError' m f = ErrorT $ catchError (runErrorT m) (fmap runErrorT f) verifyFibFile' :: FilePath -> FibIOMonad () verifyFibFile' path = do sequenceStr <- catchError' (liftIO $ readFile path) (throwError . FileUnReadable) ErrorT . return $ verifySequence sequenceStr' (0, 0)
I had asked in my question why I could not write verifyFibFile in monadic form. What I meant was, why was I unable to put in a chain of execution statements and trust that if one failed that failure would cause the entire function to immediately fail? The answer was that one particular line was failing, but the failure was being lifted into the IO context, and from that perspective looked like a success so far as the IO context was concerned. The key lies in the difference between these two lines:
contents <- liftIO $ catchError (readFile path >>= return . Right) (return . Left . FileUnreadable)
-- vs --
sequenceStr <- catchError' (liftIO $ readFile path) (throwError . FileUnreadable)
In the first line,
readFile would execute and return
Right [whatever]. catchError would simply let this pass through. If readFile failed, catchError would execute the second block. In both cases, catchError is returning a type
IO (Either a b) (where a and b just don't matter right now). This means that the failure got wrapped up as an
Either in the IO context, and that is a completely reasonable return value. To IO, this does not look like an error. The I lifted that IO context up into the FibIOMonad context, wrapping the error even deeper.
In the second case, we have
liftIO $ readFile path, which is of type
FibIOMonad String. Then, we have
throwError . FileUnreadable, which is of type
IOError -> FibIOMonad a (which probably gets forced to type
IOError -> FibIOMonad String). Both of these go into the modified
catchError', which expects an ErrorT value and a function that transforms IOError into an ErrorT, and ultimately returns an ErrorT.
The magic happens with
runErrorT and the
runErrorT :: ErrorT e m a -> m (Either e a) ErrorT :: m (Either e a) -> ErrorT e m a catchError :: MonadError e m -> m a -> (e -> m a) -> m a
Let's turn this into something significantly more concrete. In this case, here are the real data types we are working with:
runErrorT :: ErrorT FibException IO String -> IO (Either FibException String) ErrorT :: IO (Either FibException String) -> ErrorT FibException IO String catchError :: IO (Either FibException String) -> (IOError -> IO (Either FibException String)) -> IO (Either FibException String)
Here is the magic.
runErrorT contains a value that will, when evaluated, return Either a FibException or a String, but must do so in the IO context.
runErrorT returns that Either inside of the IO context.
ErrorT understands this concept and, when lifting up from the IO context to the ErrorT context, detects failures and encodes that into the context. In that last sentence, note the difference between the Constructor
ErrorT (which you can call) and the Context (or Data type)
ErrorT (which you cannot call but can put in your data type declaration).
IO $ Right [some value] becomes
Right $ IO [some value] and
IO $ Left [some error] becomes
Left $ IO [some error]. Re-wrapping. But by changing that wrapping, the Either/Error context takes effect and flow of control works in the ways defined there. Return Left from a function in the Either context will skip all future functions, while returning Right from a function will cause evaluation to continue as normal.
Let's look at this a little differently. ErrorT is a constructor for the ErrorT data type, where runErrorT is an accessor for the only public value in that data type. They are direct inverses of one another in two different senses. In the one sense, ErrorT wraps your value into an ErrorT type, where runErrorT just extracts the value. In the other sense it is all about switching the context that you are working in. Take, for example, this function:
foo :: String -> IO (Either String ()) foo val = do putStrLn val return $ if val == "bad" then Left "Bad value!" else Right ()
foo will always succeed. You can call
foo "bad" to get back a
Left "Bad value!", but that still counts as success in the IO context, and thus flow of control in the caller function will simply move on to the next action. If you want the caller to bail out when it encounters a
Left value (you actually want to use the return value to determine flow control directly) your caller needs to run in the ErrorT context, and you need to wrap foo into an ErrorT context itself:
Prelude> :t ErrorT . foo ErrorT foo :: String -> ErrorT e IO ()
You cannot directly execute
ErrorT foo in the repl because there is no
Show instance for
ErrorT e0 IO (). But if you have a function that is in the ErrorT context, you could simply "run" it.
runErrorT, again, unwraps the function into the IO context and executes the underlying action, printing the result of the action:
Prelude> :t foo foo :: IO (Either String ()) Prelude> foo "abcd" abcd Right () Prelude> runErrorT $ ErrorT $ foo "abcd" abcd Right ()
I am going to provide trivial example here. This structure becomes relevant, if you have several actions that you want to take, but having an action fail should cause the function to terminate early.
If you come from your normal imperative style of programming (Java, C, Perl, Python, etc), you will frequently have calls that could fail, and then some recovery, and then more calls that could fail, and then some more recovery. Having a function call fail might actually allow the rest of the caller to perform some recovery and complete.
You have that option here... otherwise any failure would take down your entire application. But you let go of that option when using the ErrorT context like this. The advantage is that you can put together a batch of functions that should all execute as a unit. The caller of that batch can then do the recovery and potentially call the batch again.
Using the definition of foo above, I define multifoo:
fooErrorT = ErrorT . foo multifoo :: [String] -> ErrorT String IO () multifoo (x:xs) = do fooErrorT x multifoo xs multifoo  = return () *Play Control.Monad.Error> runErrorT $ multifoo ["abcd", "efgh"] abcd efgh Right () *Play Control.Monad.Error> runErrorT $ multifoo ["abcd", "bad", "efgh"] abcd bad Left "Bad value!" *Play Control.Monad.Error> val <- runErrorT $ multifoo ["abcd", "bad", "efgh"] abcd bad *Play Control.Monad.Error> :t val val :: Either String () *Play Control.Monad.Error> val Left "Bad value!"
So, what we see here is that
multifoo, given a list of strings, will call
foo on each one in turn, until it encounters the end of the list or encounters the string "bad". If it encounters "bad",
foo will fail and
multifoo will then propogate that failure upwards.
In order to execute
multifoo, I have to call
runErrorT on it so that the
IO action gets unwrapped and evaluated. Now, remember that by unwrapping, I move the function back into the
IO context, and so at the repl,
multifoo is considered to have succeeded. Once I am working in the
IO context, I have to go back to looking at the result of
multifoo to decide what to do.
Left, do one thing (like printing an error message),
Right, do some more processing.
This article really goes into depth about why the Part 1 article works, and it provides a better alternative. In a future article, I will show how I use these techniques in some real code. For now, I leave you with this. Given how hard it was for me to figure this out, I suspect there are other Haskellers who have similar difficulties, and I hope the breakdown clarifies this piece of magick quite a lot.
Exception Handling in Haskell, Part 2 by Savanni D'Gerinel is licensed under a Creative Commons Attribution-NonCommercial-SharAlike 3.0 Unported License. You can link to it, copy it, redistribute it, and modify it, but don't sell it or the modifications and don't take my name from it.