IO, exceptions and error handling
I'm finding that a recurring theme in my work with Haskell libraries (and in particular the XML libraries) is the awkwardness of handling errors outside the IO monad. While it's often very easy to write some code that performs some function, assuming that the inputs are valid, as soon as code is required to deal with incorrect input, life gets more complicated. The choice seems to be that the functions have to be re-modelled to return a value that incorporates a possible error condition, or any such error condition results in a failure of the calling program, removing the the calling program's option to effect a recovery strategy. The exception handling system made available in the IO monad is one way out of this awkwardness, but it's only available when the recovery code is coded to run in the I/O monad (or by using unsafePerformIO). I find this is problematic when I'm trying to write a general purpose library that uses some other general-purpose library that can raise errors. I can't see any fundamental reason why exception handling has to occur in the IO monad. E.g. I'm not aware that something like this would break the Haskell type assurances: class (Eq e, Show e, Show v) => Exception e v where makeExcept :: v -> e catchExcept :: a -> (e->a) -> a throw :: e -> a instance Exception IOError String where makeExcept = userError catchExcept = catch throw e = error (show e) -- for Haskell implementations that allow errors -- to be caught in the IO monad I think a limited implementation may be possible using unsafePerformIO: data MyException = MyException String showEx (MyException s) = s instance Exception MyException String where makeExcept = MyException catchExcept a h = unsafePerformIO $ catch (return a) throw e = fail (showEx e) ... Another approach that occurs to me is to introduce an error Monad along the lines of that described by Philip Wadler as "E" in his "Essence of functional programming" paper [1]. (Or just use Either as an error monad?, which is part of what I've been doing with my XML work.) The disadvantages I see here are: (a) it requires existing code to be modified to return the error monad value. (b) it imposes a strict sequencing on the order of computation, which as far as I can see is not necessary to achieve the required error handling. For example, a computation that returns a result that is not actually used in a subsequent computation would still cause an exception; e.g. do { b <- f1 -- False ; c <- f2 -- raises exception ; d <- f3 -- value required ; return (if b then c else d) } (I know this could be coded differently to avoid the claimed problem, but to my mind it still illustrates unnecessary complexity compared with: if f1 then f2 else f3 In effect, it requires the programmer to figure out the lazy evaluation sequences instead of letting the Haskell system do it.) ... I'll note that I have reservations about using exception handling in imperative languages (because it disrupts the normal guarantees of execution flow), but I can't see any corresponding disadvatages to using exceptions in a purely functional language environment. Are there any plans or thoughts for introducing more widely usable exception handling in "Haskell 2"? #g -- [1] http://homepages.inf.ed.ac.uk/wadler/papers/essence/essence.ps and others, linked from: http://homepages.inf.ed.ac.uk/wadler/topics/monads.html (cf. section 2.3) ------------ Graham Klyne For email: http://www.ninebynine.org/#Contact
I can't see any fundamental reason why exception handling has to occur in the IO monad.
Read the paper _A Semantics for Imprecise Exceptions_. The problem is that the evaluation order of Haskell would have to be fixed for this not to lose referential transparency. What is the value of catchExcept (show (makeExcept "E1" + makeExcept "E2")) (\x -> x) ? Haskell wouldn't be "purely functional" any more. http://research.microsoft.com/~simonpj/Papers/imprecise-exn.htm --KW 8-) -- Keith Wansbrough <kw217@cl.cam.ac.uk> http://www.cl.cam.ac.uk/users/kw217/ University of Cambridge Computer Laboratory.
Now I see it. Thanks. Thanks also for the reference. Nice paper! So now where do I stand? I still think that being forced to handle exceptions in the IO monad is (sometimes) inconvenient, but I can now see why it is required for a rigorous language semantics. My problem relates to wanting to be able to effect some level of recovery in one library component for errors that arise in another. I thought about returning the entire set of exceptions, but I see that the implementation penalty is probably too high. I assume the suggested mapException function [sect 5.4] remains unproblematic ... is it (or some equivalent) actually implemented? I could imagine it being useful for mapping errors into a common framework; e.g. to catch exceptions from an XML parser and return a common "XML error" exception, incorporating information from the original as additional diagnostic. I think that the suggested unsafeIsException is appealing, since it allows some level of recovery action without (I think) opening up the insecurities of unsafePerformIO. The idea that the semantics of programs that do no raise exceptions is not affected is particularly appealing. #g -- At 14:46 14/06/04 +0100, Keith Wansbrough wrote:
I can't see any fundamental reason why exception handling has to occur in the IO monad.
Read the paper _A Semantics for Imprecise Exceptions_. The problem is that the evaluation order of Haskell would have to be fixed for this not to lose referential transparency. What is the value of
catchExcept (show (makeExcept "E1" + makeExcept "E2")) (\x -> x)
? Haskell wouldn't be "purely functional" any more.
http://research.microsoft.com/~simonpj/Papers/imprecise-exn.htm
--KW 8-) -- Keith Wansbrough <kw217@cl.cam.ac.uk> http://www.cl.cam.ac.uk/users/kw217/ University of Cambridge Computer Laboratory.
------------ Graham Klyne For email: http://www.ninebynine.org/#Contact
I assume the suggested mapException function [sect 5.4] remains unproblematic ... is it (or some equivalent) actually implemented?
http://etudiants.insia.org/~jbobbio/pafp/docs/base/Control.Exception.html#v% 3AmapException The same page has a host of other useful operations. Two useful capabilities that are missing in the actual implementation are: 1) Provision for routinely adding filename and line number. (Of course, we'd need some kind of compiler support for filling that info in.) data Exception = ... | Location SrcLoc Exception | ... 2) The ability to build up lists of exceptions perhaps by adding something like: data Exception = ... | Nested Exception Exception | ... This would allow handlers to build 'stacks' of exceptions rather like the stackdumps Java exceptions produce. For example: foo x = mapException (Nested (ErrorCall "while in foo")) $ bar x bar x = mapException (Nested (ErrorCall "while in bar")) $ baz x baz x = 1/x foo 0 ==> Nested (ErrorCall "while in foo") (Nested (ErrorCall "while in bar") (ArithException DivideByZero)) [Alternate names for 'Nested' welcome. `WhileDoing` is my next best attempt :-)] -- Alastair Reid
On Mon, 14 Jun 2004, Keith Wansbrough wrote: (snip)
to lose referential transparency. What is the value of
catchExcept (show (makeExcept "E1" + makeExcept "E2")) (\x -> x)
? Haskell wouldn't be "purely functional" any more. (snip)
We've already had these issues raised on haskell-cafe when I've been wanting non-monadic synchronous exceptions. (-: The answer is that you evaluate all branches sufficiently to discover all the exceptions raised, and maybe have an ordering on exceptions such that you can return "answers" deterministically (as a list of ones that occurred or something). I'll be happy to follow discussion of this on haskell-cafe but will be reluctant to say much that I've already said (e.g. in December 2002's "Error Handling") for fear of boring everyone silly. -- Mark
On Mon, 2004-06-14 at 14:34, Graham Klyne wrote:
I'm finding that a recurring theme in my work with Haskell libraries (and in particular the XML libraries) is the awkwardness of handling errors outside the IO monad.
With GHC You can throw exceptions in pure code but may only catch them in the IO monad. As Keith pointed out this is because the language would not be referentially transparent if you could catch exception in 'pure' code. This is often enough in most situations. If you need to catch the exceptions too then you'll want an error monad of some sort. Duncan
Graham Klyne writes:
Another approach that occurs to me is to introduce an error Monad along the lines of that described by Philip Wadler as "E" in his "Essence of functional programming" paper [1]. (Or just use Either as an error monad?, which is part of what I've been doing with my XML work.)
Control.Monad.Error defines a class MonadError and some instances that provide the functionality of Wadler's E monad.
The disadvantages I see here are: (a) it requires existing code to be modified to return the error monad value. (b) it imposes a strict sequencing on the order of computation, which as far as I can see is not necessary to achieve the required error handling. For example, a computation that returns a result that is not actually used in a subsequent computation would still cause an exception; e.g. do { b <- f1 -- False ; c <- f2 -- raises exception ; d <- f3 -- value required ; return (if b then c else d) } (I know this could be coded differently to avoid the claimed problem, but to my mind it still illustrates unnecessary complexity compared with: if f1 then f2 else f3 In effect, it requires the programmer to figure out the lazy evaluation sequences instead of letting the Haskell system do it.)
I usually do that as:
do b <- f1 if b then f2 else f3
It's only slightly more complex, it's lazy, and it works with any monad. But your general point is still valid. -- As for bridging errors from one library to another, if you use MonadError e m rather than a specific error monad, you can do something along these lines:
import Control.Monad.Error
data XmlError = X1 | X2 | X3 | XOther String deriving (Eq, Show)
instance Error XmlError where strMsg = XOther
data AppError = AppXmlError XmlError | AppOther String deriving (Eq, Show)
instance Error AppError where strMsg = AppOther
--
xmlFunc :: (MonadError XmlError m) => String -> m String xmlFunc str = throwError X1
appFunc :: (MonadError AppError m) => String -> m String appFunc s = do s' <- mapError AppXmlError (xmlFunc s) return ("result: " ++ s')
mapError :: (MonadError e m) => (e' -> e) -> ErrorT e' m a -> m a mapError f m = runErrorT m >>= either (throwError . f) return
-- n.b. runErrorT :: ErrorT e m a -> m (Either e a)
test :: String -> Either AppError String test = appFunc -- David Menendez <zednenem@psualum.com> <http://www.eyrie.org/~zednenem/>
participants (6)
-
Alastair Reid -
David Menendez -
Duncan Coutts -
Graham Klyne -
Keith Wansbrough -
Mark Carroll