Portability | portable |
---|---|
Stability | provisional |
Maintainer | gcross@phys.washington.edu |
This philosophy behind this package is that it is often better to find out all of the errors that have occured in a computation and report them simultaneously, rather than aborting as soon as the first error is encountered. Towards this end, this module supplies a type of combinable error messages so that all of the errors from subcomputations can be gathered and presented together.
The following provides an example of how these can be used:
sqrtWithError :: Float -> Either ErrorMessage Float sqrtWithError x | x < 0 = leftErrorMessageText ("Error computing the square root of " ++ (show x) ++ ":") "Square roots cannot be taken of negative numbers." | otherwise = Right (sqrt x) sumWithError :: Either ErrorMessage Float -> Either ErrorMessage Float -> Either ErrorMessage Float sumWithError (Left error1) (Left error2) = Left (error1 `mappend` error2) sumWithError (Left error) _ = Left error sumWithError _ (Left error) = Left error sumWithError (Right value1) (Right value2) = Right (value1 + value2) showSumOrErrorOf :: Float -> Float -> String showSumOrErrorOf x y = case sumWithError (sqrtWithError x) (sqrtWithError y) of Right value -> "The value is " ++ show value Left error -> show . formatErrorMessage $ error
The result of showSumOrErrorOf (-1) (-2)
is the string,
Error computing the square root of -1: Square roots cannot be taken of negative numbers. Error computing the square root of -2: Square roots cannot be taken of negative numbers.
whereas the result of showSumOrErrorOf (-1) (-1)
is the string,
Error computing the square root of -1: Square roots cannot be taken of negative numbers.
Note how the error message only appears once; this is because the process of combining the error messages automatically eliminates all identical headings under the assumption that they came from the same original computation, as was the case here.
Currently, the definition of sumWithError
is largely boilerplate.
Happily, the Haskell community has done a lot of work to identify patterns
such as these and to write libraries that allow us to express them
concisely. In particular, a standard trick when working with errors like
this is to express the calculation as a Monad
, such as by using the
following definition:
sumWithError_2 argument1 argument2 = do value1 <- argument1 value2 <- argument2 return (value1 + value2)
Or, even more concisely:
sumWithError_3 = liftM2 (+)
Unfortunately though, neither of these definitions have the same semantics
as the original sumWithError
, as using both we get the following error
message for showSumOrErrorOf (-1) (-2)
:
Error computing the square root of -1: Square roots cannot be taken of negative numbers.
That is, we have lost the second of the two error messages. The reason
for this is that Monad
-style error processing expresses the computation
as a sequence, and gives up as soon as it sees any error. In this case of
sumWithError
, however, the evaluation of the second argument can proceed
even if there was an error in the first argument. Thus, rather than using
a Monad
pattern, we use an Applicative
pattern:
sumWithError_4 = liftA2 (+)
Now both error messages are displayed.
- newtype ErrorMessage = ErrorMessage {}
- errorMessage :: String -> Doc -> ErrorMessage
- errorMessageText :: String -> String -> ErrorMessage
- errorMessageTextFromMultilineString :: String -> String -> ErrorMessage
- leftErrorMessage :: String -> Doc -> Either ErrorMessage a
- leftErrorMessageText :: String -> String -> Either ErrorMessage a
- leftErrorMessageTextFromMultilineString :: String -> String -> Either ErrorMessage a
- formatErrorMessage :: ErrorMessage -> Doc
- formatMessageWithHeading :: String -> Doc -> Doc
- gatherResultsOrErrors :: [Either e a] -> Either [e] [a]
- gatherResultsOrError :: Monoid e => [Either e a] -> Either e [a]
The ErrorMessage Type
newtype ErrorMessage Source
The ErrorMessage
type is simply a map from String
to Doc
; the reason
why the values are Doc
is because this allows us to compose them using
the combinators in Leijen's pretty-printing library.
The reason why the internal type is a Map
rather than a List is because
we assume that error message headings are unique, and so if we see the
same heading it must have come from the same error. If we did not make
such an assumption, then we would have no way of preventing the same error
from appearing several times in the message in the case that many
sub-computations all depended on the same erroneous result.
Instances for ErrorMessage
In some respects, the most important part of the ErrorMessage
type are
its instances:
- The
Monoid
instance says that we can take any two error messages and combine them usingmappend
and/ormconcat
; the implementation for this is just that of the underlying Map type. - The
Error
instance allows us to work inside theErrorT
monad usingErrorMessage
as the error type. Although it was mentioned earlier that usingApplicative
is generally preferable since it finds as many errors as possible before halting, there are times when a later computation really does need the result of an earlier computation, and in this case the sequential structure ofMonad
s exactly fits the bill.
Note that in order for ErrorMessage
to be an instance of Error
, I
needed to define how to create an ErrorMessage
without a heading
(strMsg
) and possibly without even a body (noMsg
); however, if this
ever happens, it means that the error was not handled properly --- e.g.,
when there is a pattern match failure. Thus, the heading of errors
created by noMsg
and strMsg
is
Error caused by the programmer:
Instances for Doc
Unfortunately, it does not show up in the API documentation that this
module also defines the following two instances for Doc
:
Applicative Instances
Unfortunately, it does not show up in the API documentation that this
module also defines the following two Applicative
instances:
instance (Monoid e) => Applicative (Either e) where ...
This instance declaration allows you to lift pure functions into functions
that work with values that might have errors, since both Doc
and
ErrorMessage
are instances of Monoid
. For example, we can use
liftA2 (+)
to lift the (+)
function into a function that checks both
of its arguments for errors before computing the sum. As was described
earlier, the advantage of liftA2 (+)
over liftM2 (+)
is that the
former checks for errors in both arguments and will combine them if
present, whereas the latter will ignore errors in the second argument if
there is an error in the first argument.
instance (Monoid e, Error e, Monad m) => Applicative (ErrorT e m) where pure = return e_fn <*> e_arg = ErrorT $ liftM2 (<*>) (runErrorT e_fn) (runErrorT e_arg)
This instance definition lifts the Applicative (Either e)
so that it
works for values obtained from monadic computations. Note that the
definition first executes the monad e_fn
and then the monad e_arg
, and
only after both monads have been executed in this sequence does it apply
the operator (<*>)
to the values in order to possibly combine their
error messages.
These instances allow you to write code like the following:
data Point = Point { x :: Int, y :: Int, z :: Int } pOrError = Point <$> xOrError <*> yOrError <*> zOrError
The value pOrError
is either a Point
, or a combination of the error
messages in xOrError
, yOrError
, and zOrError
.
Creation of Error Messages
Up to now we have spent a lot time discussing how to combine
ErrorMessage
s, but little time discussing how to produce them. The
provided functions for doing this are as follows:
errorMessage :: String -> Doc -> ErrorMessageSource
The function errorMessage
takes a heading and a body and produce an
ErrorMessage object from them; this can be considered to be a thin
wrapper around Data.Map.singleton
.
errorMessageText :: String -> String -> ErrorMessageSource
The function errorMessageText
is similar to the function errorMessage
,
but for the body it takes a String
instead of a Doc
. It is provided
for convenience.
errorMessageTextFromMultilineString :: String -> String -> ErrorMessageSource
Use this function when you want to create an error message from a multi-line string.
Although one could alternatively use errorMessageText
, if one were to do
this then one would only see only the first line of be indented when the
error message is formatted for output. For example,
errorMessageText "A poem:" "Roses are red.\nViolets are blue."
produces the following (formatted) error message:
A poem: Roses are red. Violets are blue.
The reason for this is because the line breaks are not known to the Doc
combinators, and so the indentation is not handled properly. The function
errorMessageTextFromMultilineString
takes care of this for you. For
example,
errorMessageTextFromMultilineString "A poem:" "Roses are red.\nViolets are blue."
produces the following (formatted) error message:
A poem: Roses are red. Violets are blue.
leftErrorMessage :: String -> Doc -> Either ErrorMessage aSource
Since one usually wants to return not just an ErrorMessage, but a value of
the form Left error_message
, the function leftErrorMessage
is provided
as a convenience; it creates the error message, and then wraps it inside
of Left
.
leftErrorMessageText :: String -> String -> Either ErrorMessage aSource
The function leftErrorMessageText
is errorMessageText
composed with
the Left
constructor for convenience.
leftErrorMessageTextFromMultilineString :: String -> String -> Either ErrorMessage aSource
The function leftErrorMessageTextFromMultilineString
is
errorMessageTextFromMultilineString
composed with the Left
constructor
for convenience.
Formatting of Error Messages
The end purpose of ErrorMessage
's existence is to be displayed to the
user. Towards this end, the following functions format an ErrorMessage
into a Doc
.
formatErrorMessage :: ErrorMessage -> DocSource
This function takes an ErrorMessage
and formats it into a Doc
. It
does this by converting the headings into text
objects, merging them
with their respective bodies (the latter having been indented by four
spaces), and then concatenating the result.
formatMessageWithHeading :: String -> Doc -> DocSource
This is the utility function used by formatErrorMessage
to format a
Doc
given a heading and a body; it indents the body by four spaces and
then appends it after the heading.
Gathering Results with Errors
Although there are many combinators available (such as liftA
and <$>)
for lifting pure functions to functions that handle errors, there are
times when one wants to gather together a list of results which might
possibly contain some errors. The following functions assist in doing
this.
gatherResultsOrErrors :: [Either e a] -> Either [e] [a]Source
This function takes a list of values which might contain errors and returns either a list of the errors found in the values or the full list of results. Note that there is no restriction on the type of the errors.
gatherResultsOrError :: Monoid e => [Either e a] -> Either e [a]Source
This function is similar to gatherResultsOrErrors
, but instead of
returning a list of errors it combines them into a single error. Note
that only restriction on the type of the error is that it be an instance
of Monoid
, so this operation is not limited to ErrorMessage
s but could
also be used for, say, Doc
s, as in the following example:
dictionary_mapping_words_to_lengths :: [(String,Int)] dictionary_mapping_words_to_lengths = [("foo",3) ,("bar",3) ] getWordLengthsOrError :: [String] -> Either ErrorMessage [Int] getWordLengthsOrError = mapLeft (errorMessage "Error looking up the following words in the dictionary:" ) . gatherResultsOrError . map lookupAndReturnResultOrError
The function call
getWordLengthsOrError ["foo","apple","cat","bar"]
results in the following error message:
Error looking up the following words in the dictionary: apple cat