Proposal: Extensible exceptions
Henning Thielemann
lemming at henning-thielemann.de
Sun Jul 13 16:37:00 EDT 2008
On Thu, 10 Jul 2008, Evan Laforge wrote:
>> As I said, in case of a bug, it is not possible to reliably clean up. That
>> an error is encountered proves that your assumptions about your program were
>> wrong, and so the assumptions about allocated resources are probably wrong,
>> too. To pick up Chris Smith's perspectives, cleaning up would be the task
>> for the next higher level, for which your error is only an exception - it
>> has hopefully kept track of the resources you allocated.
>
> How would the boundary where an error becomes an exception be defined?
> Would you have some catch error and rethrow as exception type
> function which defines the next higher level? In my experience, the
> "level boundaries" depend a lot on the individual program, and are
> fuzzy because there are big boundaries and small boundaries. Which
> ones you think of depend on how disciplined you're being and what
> level the code is at.
Within the same program you will have a very small number of such
boundaries, say one or two. As far as I can judge, in GHC there is one
such boundary. If the compiler encounters a strange behaviour of itself,
it quits with 'Panic! report bug to ...'. I think that's the most one can
do in such a case. Also a GUI driven application will have to quit in case
of an error, maybe asking the user to save his data to a different
location (not overwriting existing files, because the rescued data might
be corrupted). A server will consider its plugins as a sublevel and should
not crash, if a plugin crashes.
> Say I have (withFile something_broken). I guess there's some kind of
> cosmic-ray chance that the broken code could somehow close the file
> itself and then withFile is in trouble (let's say close a file twice
> has some dire consequence), but the chance is low enough that there
> are bigger bugs to chase. Does that mean withFile defines a level
> boundary that can reliably clean up its broken callee?
In my experience (with exceptions in an imperative language) I had cases
where some cleanup routines caused crashes, but they were actually
correct. They only crashed because something different went wrong before.
It's then hard to find the real source of the problem - but let us aside
this, since that's an debugging problem.
> It seems to me that there are lots of bugs that you can reliably clean up after.
It's certainly worth to explore that possibility, but its different from
cleaning up in case of exceptions. Cleaning up in case of an error doesn't
resolve the problem, that is, it doesn't fix the bug, but it may make you
live more comfortable with the bug.
>> It seems that nobody except me is interested in handling specifically (up to
>> a certain level) the exceptional cases that an IO action can lead to. If I
>> see the type signature
>> getLine :: IO String
>> I have no idea, what kind of exceptions can occur. I even have to be
>> prepared to get OpenGL exceptions. Even error codes in C are more
>> informative in this respect. I would have to handle some exceptions which
>> look reasonable to me, and use a catch-all or rethrow-all mechanism for the
>> rest. Possibly the proposers of the extensible exception method like to rely
>> entirely on catch-all or rethrow-all just like errors. Wouldn't
>> getLine :: ErrorT IOError IO String
>> be much clearer?
>
> It think it would be interesting, though not really solving some big
> problem I've been having,
Examples?
> though sometimes problems are obvious only in retrospect. Is there an
> implementation to experiment with?
No sorry. It would be certainly worth to work out a counter-proposal to
the currently discussed extensible exception proposal. If time allows it
... It would at least throw a new aspect into the discussion of how to
introduce "the" extensible exception modules. If there is more than one
reasonable generalization then we should not try to replace the current IO
modules with new ones, but provide both alternatives.
> But... isn't it out of the scope of this proposal? And I'm not sure
> how it fits in with the errors vs. exception distinction, or even what
> the api for errors would look like. A parallel set of throwError
> catchError etc.? Then it would just be a different kind of exception.
Would you like to differentiate the ways your program crashes? I think
division by zero, array index out of range and so on should all be handled
by quitting the program and reporting the problem to the user, and
eventually to the developer. Errors are indicated by "undefined", "error"
or just by an infinite loop. I consider "error" to be a sugared version of
an infinite loop. It's uniformly refered to as "bottom", isn't it? In case
of an infinite loop you can also not cleanup, save user data or report
anything to the user. Since referential transparency disallows distinction
of several implementations of "bottom", a routine that handles "error"s
differently from infinite loops must be a hack. But that's ok, because
that "error handling" is actually debugging, and debugging is allowed to
use hacks, just like "trace" is a hack, and an "error" that presents
values as strings without having the Show constraint, is a hack, too.
So, the "error handling" aka "debugging" system can be rather small:
indicating errors (I don't call it 'throw', differently from exceptions)
error :: String -> a
undefined :: a
assert :: Bool -> String -> a (runtime check that can be defined in terms of error)
In principle, a compiler flag could turn off the generation of code for
'error', because a correct program would never step into 'error', and thus
the flag would keep correct programs correct and buggy programs buggy. The
flag would only increase performance and decrease debugging information.
Debug.catchError :: IO a -> IO a
This is a hack that encapsulates a possibly buggy part of a program and
cancels the abort started by 'error' and 'undefined'. It can of course not
break an infinite loop or repair corrupted data. Maybe it should start
another process or some OS sandbox in order to remain unaffected by the
damages of the enclosed action. It will be needed for each layer of
"conversion" from error to exception, say one or two times in a program.
Debug.cleanupOnError :: IO () -> IO a -> IO a
Debug.cleanupOnError cleanup action = ...
This routine runs 'action' and invokes 'cleanup' if 'action' is aborted by
an error. After cleanup, it is again tried to abort the program by
"error". If 'action' terminates regularly, no further action is taken,
because it is assumed that 'action' could successfully free all acquired
resources.
There might be another routine that combines that with 'bracket' for
normal exceptions, such that a resource is freed both if an exception
occurs and if an error is encountered. Calls of this routine might become
ubiquitous in a program.
More information about the Libraries
mailing list