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 -> Bool
determines 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 a
The metaphor to grasp here is this: In the command
catch command handler
any 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.