Catching string from error function with GHC Control.Exception

I am a Haskell beginner, using GHC 6.4.1. Suppose I have a function, tt, say, which may call error. For the purposes of testing tt, I'd like to be able to catch the error string and assert that it matches the expected error string. Is HUnit or QuickCheck (or either) the appropriate tool for this sort of testing? I managed to get it working with HUnit, but it's horribly kludgy mainly because I'm yet to grok monads. The little test program below demonstrates my question outside of HUnit: import Control.Exception tt :: String -> Int tt s = error ("hi error '" ++ s ++ "'") -- tt s = read s ttWrap :: String -> IO String ttWrap x = do let r = tt x -- comment out next line, error string not caught putStr (show r) return (show r) ttWrap2 :: IO String ttWrap2 = do res <- tryJust errorCalls (ttWrap "123") case res of Right r -> return (show r) Left r -> return r main = do x <- ttWrap2 putStrLn ("caught:" ++ x) y <- ttWrap2 putStrLn ("caught:" ++ y) When you run the above test program, it does indeed catch the error string thrown by the tt function (and prints it out in main). However, if you comment out the putStr line as shown in the comment above, it no longer catches the error string, but dies instead. Why? I'm sure there is a cleaner way to achieve what I'm trying to do and I'd appreciate advice on the best way to do this. Thanks, /-\ Send instant messages to your online friends http://au.messenger.yahoo.com

On 01/01/06, Andrew Savige
I am a Haskell beginner, using GHC 6.4.1.
Suppose I have a function, tt, say, which may call error. For the purposes of testing tt, I'd like to be able to catch the error string and assert that it matches the expected error string. Is HUnit or QuickCheck (or either) the appropriate tool for this sort of testing?
I managed to get it working with HUnit, but it's horribly kludgy mainly because I'm yet to grok monads. The little test program below demonstrates my question outside of HUnit:
import Control.Exception
tt :: String -> Int tt s = error ("hi error '" ++ s ++ "'") -- tt s = read s
ttWrap :: String -> IO String ttWrap x = do let r = tt x -- comment out next line, error string not caught putStr (show r) return (show r)
ttWrap2 :: IO String ttWrap2 = do res <- tryJust errorCalls (ttWrap "123") case res of Right r -> return (show r) Left r -> return r
main = do x <- ttWrap2 putStrLn ("caught:" ++ x) y <- ttWrap2 putStrLn ("caught:" ++ y)
When you run the above test program, it does indeed catch the error string thrown by the tt function (and prints it out in main). However, if you comment out the putStr line as shown in the comment above, it no longer catches the error string, but dies instead. Why?
The error never happens until r (which is tt x) is actually evaluated. Applying putStr to (show r) will certainly demand the evaluation because it will be needed for IO, but nothing about return (show r) forces this evaluation to occur -- an unevaluated thunk will be returned. So the error doesn't happen and so doesn't get caught. The Right branch of the case is taken, which still doesn't cause a failure. The failure occurs when the value gets back to main and you try to print it. So you need to ensure the evaluation of r occurs in a particular order relative to IO. Control.Exception.evaluate does this -- it forces its parameter to be evaluated up to the top level constructor before IO proceeds, which should cause the error to occur and be caught. Note that if tt returned some more complicated structure, with the error hidden deep in the structure, you might need to use seq recursively, or just one of the DeepSeq-type libraries out there. (GHC includes Control.Parallel.Strategies which takes care of this, though it's not well documented in the libraries. myStructure `using` rnf will do the same as deepSeq does.) Hope this helps, - Cale

I've just been through the process of converting some code from using Control.Exception to using an Error monad, and I would recommend that as a more straightforward and manageable alternative, unless you need Control.Exception for other reasons. I started by changing my potentially-failing functions to return Either Exception a, where Exception is my own user-defined error type, but you could use String, and a is the type of the "real" return value. I initially used explicit Left and Right to construct appropriate values. My testing code is hand-written, and explicitly matched against Left e and Right x to decode the return value. I ended up with functions returning MonadError Exception m => m a, using throwError and return to construct appropriate values. My testing code uses ErrorT Exception (StateT s IO) a to manage a combination of error-handling, state (an error count) and IO, with the option of using Either Exception to test expected errors for individual cases. -- Iain Alexander ia@stryx.demon.co.uk
participants (3)
-
Andrew Savige
-
Cale Gibbard
-
Iain Alexander