Introduction to attempt error reporting library

I’ve just released the attempt package on hackage. It is meant to address the issue of error handling, which is currently rather ad-hoc in Haskell. It’s my hope that by putting it in its own package, we can start to standardize between packages and get some nice composable error handling between packages.

The library is built on extensible exceptions to give users the ability to return more complex exception values than is afforded by either a Maybe or (Either String). It’s similar to an (Either SomeException), but provides many class instances, a monad transformer and helper functions.

Below is an HTML version of the literate Haskell example file that is in the attempt repository. Hopefully it will give you a running start on how to use it.

This library should be considered unstable, in that the API is still open to change. As such, I’d appreciate any feedback people have.


This file is an example of how to use the attempt library, as literate
Haskell. We’ll start off with some import statements.

> {-# LANGUAGE DeriveDataTypeable #-}
> {-# LANGUAGE ExistentialQuantification #-}
> import Data.Attempt
> import Control.Monad.Attempt
> import qualified Data.Attempt.Helper as A
> import System.Environment (getArgs)
> import Safe (readMay)
> import Data.Generics
> import qualified Control.Exception as E

We’re going to deal with a very simplistic example. Let’s say you have some
text files that need processing. The files are each three lines long. The
first and last line are integers; the second is a mathematical operator (one
of +, -, * and /). Your goal with each file is to simply perform the
mathematical operator on the two numbers. Let’s start with the Operator data
type.

> data Operator = Add | Sub | Mul | Div
> instance Read Operator where
>   readsPrec _ "+" = [(Add, "")]
>   readsPrec _ "-" = [(Sub, "")]
>   readsPrec _ "*" = [(Mul, "")]
>   readsPrec _ "/" = [(Div, "")]
>   readsPrec _ s = []
>
> toFunc :: Operator -> Int -> Int -> Int
> toFunc Add = (+)
> toFunc Sub = (-)
> toFunc Mul = (*)
> toFunc Div = div

Nothing special here (besides some sloppy programming). Let’s go ahead and
write the first version of our process function.

> process1 :: FilePath -> IO Int
> process1 filePath = do
>   contents <- readFile filePath -- IO may fail for some reason
>   let [num1S, opS, num2S] = lines contents -- maybe there aren't 3 lines?
>       num1 = read num1S -- read might fail
>       op   = read opS   -- read might fail
>       num2 = read num2S -- read might fail
>   return $ toFunc op num1 num2

If you test this function out on a valid file, it works just fine. But what
happens when you call it with invalid data? In fact, there are five things
which could go wrong that I’d be interested in dealing with in the above code.

So now we need some way to deal with these issues. There’s a few standard ones
in the Haskell toolbelt:

  1. Wrap the response in Maybe. Disadvantage: can’t give any indication what he
    error was.
  2. Wrap the response in an Either String. Disadvantage: error type is simply a
    string, which isn’t necesarily very informative. Also, Either is not defined
    by the standard library to be a Monad, making this type of processing clumsy.
  3. Wrap in a more exotic Either SomeException or some such. Disadvantage:
    still not a Monad.
  4. Declare your own error type. Disadvantage: ad-hoc, and makes it very
    difficult to compose different libraries together.

In steps the attempt library. It’s essentially option 4 wrapped in a library
for general consumption. Features include:

  1. Uses extensible exceptions so you can report whatever information you want.
  2. Exceptions are not explicitly typed, so you don’t need to wrap insanely
    long function signatures to explain what exceptions you might be throwing.
  3. Defines all the standard instances you want, including providing a monad
    transformers.

    1. Attempt is a Monad.
    2. There is a Data.Attempt.Helper module which provides a special read
      function.
  4. Let’s transform the above example to use the attempt library in its most basic
    form:

    > data ProcessError = NotThreeLines String | NotInt String | NotOperator String
    >   deriving (Show, Typeable)
    > instance E.Exception ProcessError
    >
    > process2 :: FilePath -> IO (Attempt Int)
    > process2 filePath =
    >   E.handle (\e -> return $ Failure (e :: E.IOException)) $ do
    >       contents <- readFile filePath
    >       return $ case lines contents of
    >           [num1S, opS, num2S] ->
    >               case readMay num1S of
    >                   Just num1 ->
    >                       case readMay opS of
    >                           Just op ->
    >                               case readMay num2S of
    >                                   Just num2 -> Success $ toFunc op num1 num2
    >                                   Nothing -> Failure $ NotInt num2S
    >                           Nothing -> Failure $ NotOperator opS
    >                   Nothing -> Failure $ NotInt num1S
    >           _ -> Failure $ NotThreeLines contents

    If you run these on the sample files in the input directory, you’ll see that
    we’re getting the right result; the program in not erroring out, simply
    returning a failure message. However, this wasn’t very satisfactory with all of
    those nested case statements. Let’s use two facts to our advantage:

    > data ProcessErrorWrapper =
    >   forall e. E.Exception e => BadIntWrapper e
    >   | forall e. E.Exception e => BadOperatorWrapper e
    >   deriving (Typeable)
    > instance Show ProcessErrorWrapper where
    >   show (BadIntWrapper e) = "BadInt: " ++ show e
    >   show (BadOperatorWrapper e) = "BadOperator: " ++ show e
    > instance E.Exception ProcessErrorWrapper
    > process3 :: FilePath -> IO (Attempt Int)
    > process3 filePath =
    >   E.handle (\e -> return $ Failure (e :: E.IOException)) $ do
    >       contents <- readFile filePath
    >       return $ case lines contents of
    >           [num1S, opS, num2S] -> do
    >               num1 <- wrapFailure BadIntWrapper $ A.read num1S
    >               op   <- wrapFailure BadOperatorWrapper $ A.read opS
    >               num2 <- wrapFailure BadIntWrapper $ A.read num2S
    >               return $ toFunc op num1 num2
    >           _ -> Failure $ NotThreeLines contents

    That certainly cleaned stuff up. The special read function works just as you
    would expected: if the read succeeds, it returns a Success value. Otherwise,
    it returns a Failure.

    But what’s going on with that wrapFailure stuff? This is just to clean up the
    output. The read function will return an exception of type “CouldNotRead”,
    which let’s you know that you failed a read attempt, but doesn’t let you know
    what you were trying to read.

    So far, so good. But that “case lines contents” bit is still a little
    annoying. Let’s get rid of it.

    > process4 :: FilePath -> IO (Attempt Int)
    > process4 filePath =
    >   E.handle (\e -> return $ Failure (e :: E.IOException)) $ do
    >       contents <- readFile filePath
    >       return $ do
    >           let contents' = lines contents
    >           [num1S, opS, num2S] <-
    >               A.assert (length contents' == 3)
    >                        contents'
    >                        (NotThreeLines contents)
    >           num1 <- wrapFailure BadIntWrapper $ A.read num1S
    >           op   <- wrapFailure BadOperatorWrapper $ A.read opS
    >           num2 <- wrapFailure BadIntWrapper $ A.read num2S
    >           return $ toFunc op num1 num2

    There’s unfortunately no simple way to catch pattern match fails, but an
    assertion works almost as well. The only thing which is still a bit irksome is
    the whole exception handling business. Let’s be rid of that next.

    > process5 :: FilePath -> AttemptT IO Int
    > process5 filePath = do
    >   contents <- A.readFile filePath
    >   let contents' = lines contents
    >   [num1S, opS, num2S] <-
    >       A.assert (length contents' == 3)
    >                contents'
    >                (NotThreeLines contents)
    >   num1 <- wrapFailure BadIntWrapper $ A.read num1S
    >   op   <- wrapFailure BadOperatorWrapper $ A.read opS
    >   num2 <- wrapFailure BadIntWrapper $ A.read num2S
    >   return $ toFunc op num1 num2

    There’s a built-in readFile function that handles all that handling of error
    garbage for you. If you compare this version of the function to the first, you
    should notice that it’s very similar. You can avoid a lot of the common
    sources of runtime errors by simply replacing unsafe functions (Prelude.read)
    with safe ones (Data.Attempt.Helper.read).

    However, there’s still one other different between process5 and process2-4:
    the return type. process2-4 return (IO (Attempt Int)), while process5 returns
    an (AttemptT IO Int). This is the monad transformer version of Attempt; read
    the documentation for more details. To get back to the same old return type as
    before:

    > process6 :: FilePath -> IO (Attempt Int)
    > process6 = runAttemptT . process5

    Below is a simple main function for testing out these various functions. Try
    them out on the files in the input directory. Also, to simulate an IO error,
    call them on a non-existant file.

    > main = do
    >   args <- getArgs
    >   if length args /= 2
    >       then error "Usage: Example.lhs <process> <file path>"
    >       else return ()
    >   let [processNum, filePath] = args
    >   case processNum of
    >       "1" -> process1 filePath >>= print
    >       "2" -> process2 filePath >>= print
    >       "3" -> process3 filePath >>= print
    >       "4" -> process4 filePath >>= print
    >       "5" -> runAttemptT (process5 filePath) >>= print
    >       "6" -> process6 filePath >>= print
    >       x -> error $ "Invalid process function: " ++ x
About these ads

3 Responses to “Introduction to attempt error reporting library”

  1. Jose Iborra Says:

    Michael, I posted an answer to your blog here: http://pepeiborra.posterous.com/control-monad-exception-and-the-long-type-sig

    There is some controversy but definitely room for collaboration too! It would be great if you would use the MonadThrow class in your safe library.

  2. andrew Says:

    i looked for the literate haskell text, but could not get it. could you put it on github to make it available?
    thank you
    andrew

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


Follow

Get every new post delivered to your Inbox.

%d bloggers like this: