To Dream of Magick

Dreamer Shaper Seeker Maker

Exception Handling in Haskell, Part 2

Posted on Wed Feb 20 00:00:00 UTC 2013 by Savanni D'Gerinel

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.

Let's break this down

The magic happens with runErrorT and the ErrorT constructor.

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.

Reciprocity

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 ()

What is the point?

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.

Conclusion

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.

http://i.creativecommons.org/l/by-nc-sa/3.0/88x31.png

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.