The Haskell School of Expression: Learning Functional Programming Through Multimedia
by Paul Hudak
Cambridge University Press (2000) pp 239-240.
Exceptions themselves have type IOError. Among the operations allowed on this type, there are first a collection of predicates that can be used to test for a particular kind of exception. For example, this predicate:
isEOFError :: IOError -> Booldetermines whether an end-of-file exception has occurred. (Note:)
More importantly, there is a function catch, which does the actual exception handling. The first argument to catch is an IO action that we are attempting to execute. The second argument is an exception handler of type IOError -> IO a.
catch :: IO a -> (IOError -> IO a) -> IO aThe metaphor to grasp here is this: In the command
catch command handlerany exception that occurs in command (which may generate a long sequence of actions, perhaps even infinite) gets "thrown" outward, to be "caught" by the exception handler handler. Control is effectively transferred to the handler by applying it to the IOError that occurred. If command succeeds, the handler is ignored.
For example, this version of getChar returns a newline character if any kind of exception is encountered:
getChar1 :: IO Char getChar1 = catch getChar (\e -> return '\n')However, this is rather crude because it treats all exceptions in the same manner. If only the end-of-file exception is to be recognized, the IOError value must be queried:
getChar1 :: IO Char getChar1 = catch getChar (\e -> if isEOFError e then return '\n' else ioError e)The ioError function used here "throws" the exception upward to the next exception handler. In other words, nested calls to catch are permitted, and produce nested exception handlers. The function ioError may be called from within a normal action sequence or from within an exception handler as in the getChar1 example above.
Using getChar1 we can redefine getLine to demonstrate the use of nested handlers:
getLine1 :: IO String getLine1 = catch getLine2 (\err -> return ("Error: "++(show err)) where getLine2 = do c <- getChar1 if c == '\n' then return "" else do l <- getLine1 return (c:l)Note how "looping" is achieved simply by a recursive call to getline1. Nested exception handlers allow getChar1 to catch the end-of-file exception while any other exception results in a string starting with "Error: " to be returned from getLine1.
If you do not provide an exception handler for your program, or fail to catch a particular exception that occurs, Haskell invokes a default exception handler that prints out the exception and terminates your program.
You might wonder why the Haskell designers did not just define IOError as a datatype that enumerated all possible IO errors. The main reason is that some implementations may support different kinds of such errors, and datatypes in Haskell are not extensible. Although the predicate approach is more cumbersome to use, it is easy to add support for a new IO error by simply including an additional predicate.