error-message 1.0.1 → 1.1
raw patch · 4 files changed
+279/−43 lines, 4 filesdep ~InfixApplicativedep ~ansi-wl-pprintdep ~basesetup-changedPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: InfixApplicative, ansi-wl-pprint, base, containers, either-unwrap, mtl
API changes (from Hackage documentation)
- Data.ErrorMessage: instance Monoid Doc
+ Data.ErrorMessage: type ErrorMessageOr = Either ErrorMessage
Files
- Data/ErrorMessage.hs +196/−5
- Setup.hs +4/−0
- error-message.cabal +16/−8
- tests/runtests.hs +63/−30
Data/ErrorMessage.hs view
@@ -1,5 +1,11 @@+-- @+leo-ver=4-thin+-- @+node:gcross.20091202203048.1603:@thin ErrorMessage.hs+-- @@language Haskell+-- @@raw ------------------------------------------------------------------------------ -- |+-- @@end_raw+-- @+at -- Module : Data.ErrorMessage -- Copyright : (c) Gregory Crosswhite -- License : BSD-style@@ -16,6 +22,9 @@ -- -- The following provides an example of how these can be used: -- +-- @-at+-- @@c+-- @@raw -- > sqrtWithError :: Float -> Either ErrorMessage Float -- > sqrtWithError x -- > | x < 0@@ -36,6 +45,8 @@ -- > case sumWithError (sqrtWithError x) (sqrtWithError y) of -- > Right value -> "The value is " ++ show value -- > Left error -> show . formatErrorMessage $ error+-- @@end_raw+-- @+at -- -- The result of @showSumOrErrorOf (-1) (-2)@ is the string, -- @@ -88,14 +99,22 @@ -- -- Now both error messages are displayed. -- +-- @-at+-- @@c+-- @@raw ------------------------------------------------------------------------------+-- @@end_raw -{-# LANGUAGE GeneralizedNewtypeDeriving #-}+-- @<< Language extensions >>+-- @+node:gcross.20091202203048.7000:<< Language extensions >>+-- @-node:gcross.20091202203048.7000:<< Language extensions >>+-- @nl module Data.ErrorMessage ( -- * The ErrorMessage Type ErrorMessage(..)+ ,ErrorMessageOr -- $error_message_type -- ** Instances for ErrorMessage@@ -135,6 +154,8 @@ ,gatherResultsOrError ) where +-- @<< Import needed modules >>+-- @+node:gcross.20091202203048.1605:<< Import needed modules >> import Control.Arrow import Control.Applicative hiding (empty) import Control.Applicative.Infix@@ -150,8 +171,20 @@ import qualified Data.Map as Map import Text.PrettyPrint.ANSI.Leijen+-- @-node:gcross.20091202203048.1605:<< Import needed modules >>+-- @nl +-- @+others+-- @+node:gcross.20091202203048.1606:Types+-- @+node:gcross.20091202203048.1607:ErrorMessage newtype ErrorMessage = ErrorMessage { unwrapErrorMessage :: Map String Doc }+type ErrorMessageOr = Either ErrorMessage+-- @-node:gcross.20091202203048.1607:ErrorMessage+-- @-node:gcross.20091202203048.1606:Types+-- @+node:gcross.20091202203048.1608:Instances+-- @+node:gcross.20091202203048.1609:Applicative (Either e a)+-- @@raw+-- @@end_raw instance (Monoid e) => Applicative (Either e) where pure = Right@@ -159,54 +192,92 @@ (<*>) (Left error) _ = Left error (<*>) _ (Left error) = Left error (<*>) (Right function) (Right argument) = Right (function argument)+-- @-node:gcross.20091202203048.1609:Applicative (Either e a)+-- @+node:gcross.20091202203048.1610:Applicative (ErrorT e m a) 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)+-- @-node:gcross.20091202203048.1610:Applicative (ErrorT e m a)+-- @+node:gcross.20091202203048.1611:Error ErrorMessage instance Error ErrorMessage where noMsg = strMsg "(and he did not even bother to include an error message! :-/)" strMsg = errorMessage "Error caused by the programmer:" . text+-- @-node:gcross.20091202203048.1611:Error ErrorMessage+-- @+node:gcross.20091202203048.7025:Monoid ErrorMessage instance Monoid ErrorMessage where mempty = ErrorMessage Map.empty mappend (ErrorMessage a) (ErrorMessage b) = ErrorMessage (mappend a b) mconcat = ErrorMessage . mconcat . map unwrapErrorMessage+-- @-node:gcross.20091202203048.7025:Monoid ErrorMessage+-- @+node:gcross.20091202203048.1613:Error Doc instance Error Doc where noMsg = empty strMsg = text-instance Monoid Doc where- mempty = empty- mappend = (<$$>)- mconcat = vcat+-- @-node:gcross.20091202203048.1613:Error Doc+-- @+node:gcross.20091202203048.1612:Monoid Doc+-- @-node:gcross.20091202203048.1612:Monoid Doc+-- @-node:gcross.20091202203048.1608:Instances+-- @+node:gcross.20091202203048.1614:Functions+-- @+node:gcross.20091202203048.7038:Error Message Creation+-- @+node:gcross.20091202203048.1619:errorMessage / leftErrorMessage+-- @@raw -- |+-- @@end_raw+-- @+at -- 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'.+-- @-at+-- @@c errorMessage :: String -> Doc -> ErrorMessage errorMessage heading = ErrorMessage . Map.singleton heading +-- @@raw -- |+-- @@end_raw+-- @+at -- 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'.+-- @-at+-- @@c leftErrorMessage :: String -> Doc -> Either ErrorMessage a leftErrorMessage heading = Left . errorMessage heading+-- @-node:gcross.20091202203048.1619:errorMessage / leftErrorMessage+-- @+node:gcross.20091202203048.1620:errorMessageText / leftErrorMessageText+-- @@raw -- |+-- @@end_raw+-- @+at -- 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.+-- @-at+-- @@c errorMessageText :: String -> String -> ErrorMessage errorMessageText heading = errorMessage heading . text +-- @@raw -- |+-- @@end_raw+-- @+at -- The function 'leftErrorMessageText' is 'errorMessageText' composed with -- the 'Left' constructor for convenience.+-- @-at+-- @@c leftErrorMessageText :: String -> String -> Either ErrorMessage a leftErrorMessageText heading = Left . errorMessageText heading+-- @-node:gcross.20091202203048.1620:errorMessageText / leftErrorMessageText+-- @+node:gcross.20091202203048.1621:errorMessageTextFromMultilineString / leftErrorMessageTextFromMultilineString+-- @@raw -- |+-- @@end_raw+-- @+at -- Use this function when you want to create an error message from a -- multi-line string. -- @@ -227,7 +298,12 @@ -- 'errorMessageTextFromMultilineString' takes care of this for you. For -- example, -- +-- @-at+-- @@c+-- @@raw -- > errorMessageTextFromMultilineString "A poem:" "Roses are red.\nViolets are blue."+-- @@end_raw+-- @+at -- -- produces the following (formatted) error message: -- @@ -235,22 +311,38 @@ -- > Roses are red. -- > Violets are blue. -- +-- @-at+-- @@c errorMessageTextFromMultilineString :: String -> String -> ErrorMessage errorMessageTextFromMultilineString heading = errorMessage heading . vcat . map text . lines +-- @@raw -- |+-- @@end_raw+-- @+at -- The function 'leftErrorMessageTextFromMultilineString' is -- 'errorMessageTextFromMultilineString' composed with the 'Left' constructor -- for convenience.+-- @-at+-- @@c leftErrorMessageTextFromMultilineString :: String -> String -> Either ErrorMessage a leftErrorMessageTextFromMultilineString heading = Left . errorMessageTextFromMultilineString heading+-- @-node:gcross.20091202203048.1621:errorMessageTextFromMultilineString / leftErrorMessageTextFromMultilineString+-- @-node:gcross.20091202203048.7038:Error Message Creation+-- @+node:gcross.20091202203048.7039:Formatting+-- @+node:gcross.20091202203048.1617:formatErrorMessage+-- @@raw -- |+-- @@end_raw+-- @+at -- 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.+-- @-at+-- @@c formatErrorMessage :: ErrorMessage -> Doc formatErrorMessage =@@ -261,26 +353,47 @@ Map.assocs . unwrapErrorMessage+-- @-node:gcross.20091202203048.1617:formatErrorMessage+-- @+node:gcross.20091202203048.1618:formatMessageWithHeading+-- @@raw -- |+-- @@end_raw+-- @+at -- 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.+-- @-at+-- @@c formatMessageWithHeading :: String -> Doc -> Doc formatMessageWithHeading heading body = text heading <$$> indent 4 body+-- @-node:gcross.20091202203048.1618:formatMessageWithHeading+-- @-node:gcross.20091202203048.7039:Formatting+-- @+node:gcross.20091202203048.7040:Extracting results from a list+-- @+node:gcross.20091202203048.1616:gatherResultsOrErrors+-- @@raw -- |+-- @@end_raw+-- @+at -- 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.+-- @-at+-- @@c gatherResultsOrErrors :: [Either e a] -> Either [e] [a] gatherResultsOrErrors eithers = case partitionEithers (eithers) of ([],results) -> Right results (errors,_) -> Left errors+-- @-node:gcross.20091202203048.1616:gatherResultsOrErrors+-- @+node:gcross.20091202203048.1615:gatherResultsOrError+-- @@raw -- |+-- @@end_raw+-- @+at -- 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 @@ -314,10 +427,23 @@ -- > apple -- > cat -- +-- @-at+-- @@c gatherResultsOrError :: Monoid e => [Either e a] -> Either e [a] gatherResultsOrError = mapLeft mconcat . gatherResultsOrErrors+-- @-node:gcross.20091202203048.1615:gatherResultsOrError+-- @-node:gcross.20091202203048.7040:Extracting results from a list+-- @-node:gcross.20091202203048.1614:Functions+-- @-others +-- @<< Documentation sections >>+-- @+node:gcross.20091202203048.7003:<< Documentation sections >>+-- @+others+-- @+node:gcross.20091202203048.7004:ErrorMessage type+-- @@raw -- $error_message_type+-- @@end_raw+-- @+at -- 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.@@ -328,10 +454,16 @@ -- 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.+-- @-at+-- @@c +-- @+node:gcross.20091202203048.7027:ErrorMessage instances+-- @@raw -- $error_message_instances+-- @@end_raw+-- @+at -- In some respects, the most important part of the 'ErrorMessage' type are -- its instances: -- @@ -353,9 +485,16 @@ -- when there is a pattern match failure. Thus, the heading of errors -- created by 'noMsg' and 'strMsg' is -- /Error caused by the programmer:/+-- @-at+-- @@c +-- @-node:gcross.20091202203048.7027:ErrorMessage instances+-- @+node:gcross.20091202203048.7029:Doc instances+-- @@raw -- $doc_instances+-- @@end_raw+-- @+at -- Unfortunately, it does not show up in the API documentation that this -- module also defines the following two instances for 'Doc': -- @@ -365,13 +504,25 @@ -- * The 'Error' instance defines a 'noMsg' error to be the 'empty' 'Doc' and -- the 'strMsg' error to be a 'text' 'Doc'. -- +-- @-at+-- @@c +-- @-node:gcross.20091202203048.7029:Doc instances+-- @+node:gcross.20091202203048.7031:Applicative instances+-- @@raw -- $applicative_instances+-- @@end_raw+-- @+at -- Unfortunately, it does not show up in the API documentation that this -- module also defines the following two 'Applicative' instances: -- +-- @-at+-- @@c+-- @@raw -- > instance (Monoid e) => Applicative (Either e) where ...+-- @@end_raw+-- @+at -- -- This instance declaration allows you to lift pure functions into functions -- that work with values that might have errors, since both 'Doc' and @@ -383,9 +534,14 @@ -- present, whereas the latter will ignore errors in the second argument if -- there is an error in the first argument. -- +-- @-at+-- @@c+-- @@raw -- > 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)+-- @@end_raw+-- @+at -- -- This instance definition lifts the @Applicative (Either e)@ so that it -- works for values obtained from monadic computations. Note that the @@ -396,29 +552,64 @@ -- -- These instances allow you to write code like the following: -- +-- @-at+-- @@c+-- @@raw -- > data Point = Point { x :: Int, y :: Int, z :: Int } -- > -- > pOrError = Point <$> xOrError <*> yOrError <*> zOrError+-- @@end_raw+-- @+at -- -- The value @pOrError@ is either a @Point@, or a combination of the error -- messages in @xOrError@, @yOrError@, and @zOrError@. -- +-- @-at+-- @@c +-- @-node:gcross.20091202203048.7031:Applicative instances+-- @-node:gcross.20091202203048.7004:ErrorMessage type+-- @+node:gcross.20091202203048.7032:Creating Error Messages+-- @@raw -- $error_message_creation+-- @@end_raw+-- @+at -- 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:+-- @-at+-- @@c +-- @-node:gcross.20091202203048.7032:Creating Error Messages+-- @+node:gcross.20091202203048.7042:Formatting Error Messages+-- @@raw -- $error_message_formatting+-- @@end_raw+-- @+at -- 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'.+-- @-at+-- @@c +-- @-node:gcross.20091202203048.7042:Formatting Error Messages+-- @+node:gcross.20091202203048.7044:Gathering Results with Errors+-- @@raw -- $gathering_results_with_errors+-- @@end_raw+-- @+at -- 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.+-- @-at+-- @@c +-- @-node:gcross.20091202203048.7044:Gathering Results with Errors+-- @-others+-- @-node:gcross.20091202203048.7003:<< Documentation sections >>+-- @nl+-- @-node:gcross.20091202203048.1603:@thin ErrorMessage.hs+-- @-leo
Setup.hs view
@@ -1,3 +1,5 @@+-- @+leo-ver=4-thin+-- @+node:gcross.20091202203048.6991:@thin Setup.hs import Control.Monad import Distribution.Simple import System.Exit@@ -22,3 +24,5 @@ putStrLn "" system "runhaskell -i. -i./tests tests/runtests.hs" return ()+-- @-node:gcross.20091202203048.6991:@thin Setup.hs+-- @-leo
error-message.cabal view
@@ -1,10 +1,13 @@+-- @+leo-ver=4-thin+-- @+node:gcross.20091202203048.1623:@thin error-message.cabal+-- @@language Haskell Name: error-message-Version: 1.0.1+Version: 1.1 Cabal-Version: >= 1.6 License: BSD3 License-File: LICENSE Author: Gregory Crosswhite-Copyright: (c) 2009 Gregory Crosswhite+Copyright: (c) 2009-2010 Gregory Crosswhite Maintainer: gcross@phys.washington.edu Stability: stable Homepage: http://github.com/gcross/error-message@@ -18,14 +21,19 @@ 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.+ .+ New in version 1.1: Removed Monoid instance for Doc (it should now be supplied by ansi-wl-pprint), added ErrorMessageOr type alias. Extra-Source-Files: tests/runtests.hs Library Exposed: True Exposed-Modules: Data.ErrorMessage- Build-Depends: base == 4.*,- mtl == 1.*,- containers == 0.2.*,- either-unwrap >= 1.1 && < 2,- InfixApplicative == 1.*,- ansi-wl-pprint == 0.5.*+ Build-Depends: base >= 4.1 && < 4.3,+ mtl >= 1.1 && < 1.2,+ containers >= 0.2 && < 0.4,+ either-unwrap >= 1.1 && < 1.2,+ InfixApplicative >= 1.0 && < 1.2,+ ansi-wl-pprint >= 0.5 && < 0.6.4+-- @nonl+-- @-node:gcross.20091202203048.1623:@thin error-message.cabal+-- @-leo
tests/runtests.hs view
@@ -1,3 +1,9 @@+-- @+leo-ver=4-thin+-- @+node:gcross.20091202203048.6990:@thin runtests.hs+-- @@language Haskell++-- @<< Import needed modules >>+-- @+node:gcross.20091202203048.6992:<< Import needed modules >> import Control.Applicative import Control.Monad @@ -10,12 +16,21 @@ import Test.HUnit import Text.PrettyPrint.ANSI.Leijen+-- @-node:gcross.20091202203048.6992:<< Import needed modules >>+-- @nl +-- @+others+-- @+node:gcross.20091202203048.7047:Values+-- @+node:gcross.20091202203048.7049:dictionary_mapping_words_to_lengths dictionary_mapping_words_to_lengths :: [(String,Int)] dictionary_mapping_words_to_lengths = [("foo",3) ,("bar",3) ]+-- @-node:gcross.20091202203048.7049:dictionary_mapping_words_to_lengths+-- @-node:gcross.20091202203048.7047:Values+-- @+node:gcross.20091202203048.6995:Functions+-- @+node:gcross.20091202203048.7002:sqrtWithError sqrtWithError :: Float -> Either ErrorMessage Float sqrtWithError x | x < 0@@ -24,58 +39,73 @@ "Square roots cannot be taken of negative numbers." | otherwise = Right (sqrt x)+-- @nonl+-- @-node:gcross.20091202203048.7002:sqrtWithError+-- @+node:gcross.20091202203048.7013:sumWithError+-- @+node:gcross.20091202203048.7001:sumWithError 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) +-- @-node:gcross.20091202203048.7001:sumWithError+-- @+node:gcross.20091202203048.7006:sumWithError_2 sumWithError_2 argument1 argument2 = do value1 <- argument1 value2 <- argument2 return (value1 + value2)+-- @-node:gcross.20091202203048.7006:sumWithError_2+-- @+node:gcross.20091202203048.7012:sumWithError_3 sumWithError_3 = liftM2 (+)+-- @-node:gcross.20091202203048.7012:sumWithError_3+-- @+node:gcross.20091202203048.7020:sumWithError_4 sumWithError_4 :: Either ErrorMessage Float -> Either ErrorMessage Float -> Either ErrorMessage Float sumWithError_4 = liftA2 (+)+-- @-node:gcross.20091202203048.7020:sumWithError_4+-- @-node:gcross.20091202203048.7013:sumWithError+-- @+node:gcross.20091202203048.7014:showSumOrErrorOf+-- @+node:gcross.20091202203048.6997:showSumOrErrorOf 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+-- @nonl+-- @-node:gcross.20091202203048.6997:showSumOrErrorOf+-- @+node:gcross.20091202203048.7016:showSumOrErrorOf_2 showSumOrErrorOf_2 :: Float -> Float -> String showSumOrErrorOf_2 x y = case sumWithError_2 (sqrtWithError x) (sqrtWithError y) of Right value -> "The value is " ++ show value Left error -> show . formatErrorMessage $ error+-- @-node:gcross.20091202203048.7016:showSumOrErrorOf_2+-- @+node:gcross.20091202203048.7018:showSumOrErrorOf_3 showSumOrErrorOf_3 :: Float -> Float -> String showSumOrErrorOf_3 x y = case sumWithError_3 (sqrtWithError x) (sqrtWithError y) of Right value -> "The value is " ++ show value Left error -> show . formatErrorMessage $ error+-- @-node:gcross.20091202203048.7018:showSumOrErrorOf_3+-- @+node:gcross.20091202203048.7024:showSumOrErrorOf_4 showSumOrErrorOf_4 :: Float -> Float -> String showSumOrErrorOf_4 x y = case sumWithError_4 (sqrtWithError x) (sqrtWithError y) of Right value -> "The value is " ++ show value Left error -> show . formatErrorMessage $ error-lookupAndReturnResultOrError :: String -> Either Doc Int-lookupAndReturnResultOrError word =- case lookup word dictionary_mapping_words_to_lengths of- Nothing -> Left (text word)- Just word_length -> Right word_length-getWordLengthsOrError :: [String] -> Either ErrorMessage [Int]-getWordLengthsOrError =- mapLeft- (errorMessage- "Error looking up the following words in the dictionary:"- )- .- gatherResultsOrError- .- map lookupAndReturnResultOrError-+-- @-node:gcross.20091202203048.7024:showSumOrErrorOf_4+-- @-node:gcross.20091202203048.7014:showSumOrErrorOf+-- @-node:gcross.20091202203048.6995:Functions+-- @-others main = defaultMain $+ -- @ << Tests >>+ -- @+node:gcross.20091202203048.6996:<< Tests >>+ -- @+others+ -- @+node:gcross.20091202203048.6993:Documentation examples [testGroup "Documentation examples"+ -- @ @+others+ -- @+node:gcross.20091202203048.6994:showSumOrErrorOf [testGroup "showSumOrErrorOf" $ [testCase "(-1) (-2)" $ assertEqual@@ -100,6 +130,8 @@ $ showSumOrErrorOf (-1) (-1) ]+ -- @-node:gcross.20091202203048.6994:showSumOrErrorOf+ -- @+node:gcross.20091202203048.7008:showSumOrErrorOf_2 ,testGroup "showSumOrErrorOf_2" $ [testCase "(-1) (-2)" $ assertEqual@@ -112,6 +144,8 @@ $ showSumOrErrorOf_2 (-1) (-2) ]+ -- @-node:gcross.20091202203048.7008:showSumOrErrorOf_2+ -- @+node:gcross.20091202203048.7010:showSumOrErrorOf_3 ,testGroup "showSumOrErrorOf_3" $ [testCase "(-1) (-2)" $ assertEqual@@ -124,6 +158,8 @@ $ showSumOrErrorOf_3 (-1) (-2) ]+ -- @-node:gcross.20091202203048.7010:showSumOrErrorOf_3+ -- @+node:gcross.20091202203048.7022:showSumOrErrorOf_4 ,testGroup "showSumOrErrorOf_3" $ [testCase "(-1) (-2)" $ assertEqual@@ -138,20 +174,8 @@ $ showSumOrErrorOf_4 (-1) (-2) ]- ,testCase "getWordLengthsOrError" $- assertEqual- "Was the expected message produced?"- (Left . show . vcat . map text $- ["Error looking up the following words in the dictionary:"- ," apple"- ," cat"- ]- )- .- mapLeft (show . formatErrorMessage)- $- getWordLengthsOrError ["foo","apple","cat","bar"]-+ -- @-node:gcross.20091202203048.7022:showSumOrErrorOf_4+ -- @+node:gcross.20091203120301.1651:Multiline strings ,testGroup "Multiline strings" $ [testCase "errorMessageText" $ assertEqual@@ -184,5 +208,14 @@ $ errorMessageTextFromMultilineString "A poem:" "Roses are red.\nViolets are blue." ]+ -- @-node:gcross.20091203120301.1651:Multiline strings+ -- @-others ]+ -- @-node:gcross.20091202203048.6993:Documentation examples+ -- @-others+ -- @nonl+ -- @-node:gcross.20091202203048.6996:<< Tests >>+ -- @nl ]+-- @-node:gcross.20091202203048.6990:@thin runtests.hs+-- @-leo