exceptions again

The topic of exceptions has come up plenty of times on LtU, with concomitant good food for thought discussions. But I'm not sure I've yet to see a real break down of the issue into basic components from which one could consider designs for languages when it comes to not/doing error handling. Does anybody know of some theory or math or something more solid that is an eye on error handling? Unfortunately, even with that, it sounds like good design for error handling also has to take into consideration subjective usability. And, there is a lot of important nuance in how the 'basic components' interact - yay combinatorics.

Random examples of what I mean / am trying to find / understand: Error codes vs. exceptions (dealing with clarity of non-error code flow). Java differentiates between Errors and Exceptions (dealing with the fact that some exceptions are more exceptional than others). C++ advertises RAII (dealing with the fact that you want a sane way of creating-destroying things in light of errors). Erlang eschews threads (dealing with the fact that threads can make exception handling way more complicated). Option types are useful (dealing with the fact that some errors aren't exceptional). D supports scope-exit (dealing with clarity of error code flow).

Is there something which helps us get at all these issues more rigorously and sanely?

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Not to mention...

Mondas, such as the maybe monad, and other types of deferred exception objects--IEEE 754 signalling NaN is another example.

Bottom propogation in general (think NaN)--use of "error" values which, when passed into other functions, yield--more error values. Until somebody notices that an error state is being passed around.

The exception semantics of Eiffel--wherein routines may have postconditions (promises) which must hold, else an exception must be raised, is also interesting to consider.

Maybe - monad and type

In what way does making Maybe a monad add to its value for error handling beyond what you get as a discriminated union? Is there some extra sauce that you get beyond what something like boost::optional provides in a non-monadic language?

Other than surface syntax issues,

Maybe and boost::optional are (mostly) the same thing. Other than the fact the Boost developers are careful not to use scary words like "monad" in mixed company. :)

Maybe (and boost::optional) does provide semantics above and beyond an arbitrary discriminated union or sum type.

A few older LtU threads may be of interest:

this and this

I don't have time for a

I don't have time for a really good answer, but basically, you do get some extra sauce. Basically, you get the ability to take a bunch of computations, each of which may fail, and string them together as though none of them fail. The result of the whole thing is a discriminated union, so you ignore failure until the very end. This hides the discriminated union and becomes basically as convenient as traditional exception handling. I don't know whether boost::optional lets you compose operations in this way.

defered bottom up error propagation

Undoing Dynamic Typing shows a clever solution to localizing deferred errors (in the context of in the interfacing statically-typed languages with dynamic-typed languages) by using control operators to rollback to the point where the error was introduced.

Exceptions in C--

raould,
this paper,A Single Intermediate Language That Supports Multiple Implementations of Exceptions discusses exception implementations and design trade offs between them. It also notes side affects of the implementation on other features like calling conventions.

My view - feedback appreciated!

I don't like unchecked expcetions, but I also don't like how checked exceptions are handled in Java. Perhaps it's that I just don't like the distinction between the typical behaviour of a system, and what some might consider its "exceptional" behaviour. A system's behaviour, is its behaviour, and I think such a distinction leads to sloppiness. Thus, given an expressive language with algebraic data types and pattern matching, I think all cases, including errors, should be reflected in return values. Simple and effective.

This has the flavour of a total language. I realize that totality can be a tad cumbersome, but this can be mitigated somewhat (I hope) by two factors:
1. careful consideration of the interfaces to require as few error cases as possible (many libraries are too sloppy with errors; why does C#'s IDictionary<K,T> throw an exception for duplicate keys?),

2. monadic structures to handle and propagate errors.

How do others find exceptions in Haskell? What might the performance implications for monadic code be, particularly for JIT-compiled languages? I'm just wondering if it's feasible to use monads in a jitted language, or if monadic code requires too much optimization.

What's the aim here?

Monads can be used to encode an out-of-band return mechanism very similar to what's in use in Java (though you have a wide range of choices). It's not clear to me from your post whether your objections would apply to such an encoding.

I don't think I would object

I don't think I would object to it, as the mechanism used is not specific to exceptions, and the behaviour of the underlying object is fully specified algebraically (correct me if I'm wrong). So the "exceptions" are not out of band information, they are simply handled in a fashion where we can propagate them as out of band information should we like to, which I think is better.

I was merely wondering about the runtime costs of monadic structured code, how much optimization is needed for suitably efficient execution, etc. I suppose it depends how pervasive the use of monads is, but let's say it's limited to taming effects and exceptions (and not say, to CPS transform the program).

a time and a place

i think the fact that language-systems today have only one way to represent themselves is part of the problem. sometimes i very much don't want to see the error code. other times i only want to see the error handling. other times i want to see everything together.

I'm curious: in which

I'm curious: in which situations do you not want to see the errors? I'm just wondering what you're trying to achieve while ignoring the edge cases, ie. what type of development are you typically doing in such situations: debugging, testing, writing extensions, ...?

When there's "nothing" to do

Using error code for every errors is tedious as you see in the code all the special handling which must be chained even if you just want to report .. that's there was an error.

With exceptions the unrolling happen automatically which reduce the size of the code, too bad programming with exceptions is hard.

Using error code for every

Using error code for every errors is tedious as you see in the code all the special handling which must be chained even if you just want to report .. that's there was an error.

Yes, this is a problem with totality when combined with closed sums. I'm looking into polymorphic variants to hopefully alleviate this somewhat. Of course, I expect actual error conditions to be fewer in total languages *because* it's somewhat tedious. I think that's a good thing actually.

[Edit: I think the cases where there's "nothing to do" are actually a symptom of this problem, namely, throwing exceptions for situations which aren't really exceptional at all. I conjecture that total languages would reduce this silliness. There's always something to do for a true error, ie. prompt the user to retry, kill a misbehaving agent, etc.]

Propagation, external stimuli, and context

[...] tedious as you see [...] all the special handling which must be chained [...]
Yes, this is a problem with totality when combined with closed sums. I'm looking into polymorphic variants to hopefully alleviate this somewhat.

I don't immediately see how polymorphic variants could make it easier to propagate (or chain) error conditions. OTOH, I could imagine something like polymorphic variants, rather than an open (generative) datatype, to be used as a type for exceptions (or errors) to track which cases have been handled, but that is a different problem.

Indeed, I think that the actual problem with exceptions is knowing whether you've handled all of them. It would be useful to be able to say that no exceptions can be propagated out of a particular expression.

Of course, I expect actual error conditions to be fewer in total languages *because* it's somewhat tedious.

I'm not entirely sure what you intend to mean when you say "actual error condition". According to Merriam-Webster, "actual" means "existing in fact and not merely as a possibility".

Note that programs typically need to handle two rather different kinds of error conditions: error conditions resulting from external (potentially physical) stimuli (e.g. corrupted file, network connection that fails (when the line is cut), ...), and error conditions resulting from internal software defects (bugs).

Error conditions from external stimuli cannot be eliminated by even proving the program correct. If you implement two equivalent programs, one in a total language and another in a partial language, surely they will have to handle all the same error conditions resulting from external stimuli.

OTOH, a total language might encourage (or even force) you to pay more attention to the correctness of algorithms within the program. While this would likely reduce the number of "error" conditions required to be handled by the program, it would likely increase the number of "ordinary" conditions handled by the program. I'm not saying that this would be a bad thing, but there is a trade-off.

[Edit: I think the cases where there's "nothing to do" are actually a symptom of this problem, namely, throwing exceptions for situations which aren't really exceptional at all. I conjecture that total languages would reduce this silliness. There's always something to do for a true error, ie. prompt the user to retry, kill a misbehaving agent, etc.]

I think that is misunderstanding (or misrepresenting) the role of exception handling. Sure, there is usually something to do --- given a proper context. The difficulty is that at the point in the program where you detect a problem, there might not be anything reasonable to do. This is particularly a problem with writing reusable components. At the point you detect an error in a reusable component, what can you reasonably do? The proper action depends on the context in which the component is used. The best thing you can do is to let the error be handled by another part of the program where a reasonable action can be taken. Exception handling is one way to allow that without having to explicitly propagate errors everywhere. It allows an exception handler to be defined (only) at the point in a program where a proper action can be taken.

I don't immediately see how

I don't immediately see how polymorphic variants could make it easier to propagate (or chain) error conditions.

I'm thinking of something along the lines of First-class Messages with Match Functions, with some extensions.

I'm not entirely sure what you intend to mean when you say "actual error condition". According to Merriam-Webster, "actual" means "existing in fact and not merely as a possibility".

I mean errors that result from truly exceptional circumstances, such as failing hard disks, rather than from language constructs whose partiality results in "errors" (like casting exceptions, null pointer exceptions, etc.).

Error conditions from external stimuli cannot be eliminated by even proving the program correct. If you implement two equivalent programs, one in a total language and another in a partial language, surely they will have to handle all the same error conditions resulting from external stimuli.

Yes. The total language should also have already specified these edge cases, which means it has a greater chance of continuing or at least failing gracefully.

it would likely increase the number of "ordinary" conditions handled by the program. I'm not saying that this would be a bad thing, but there is a trade-off.

Well, there's nothing stopping you from using a default case and just exiting the program. That's what would happen in a partial program for unhandled exceptions, so no convenience lost IMO.

Sure, there is usually something to do --- given a proper context. The difficulty is that at the point in the program where you detect a problem, there might not be anything reasonable to do. [...] Exception handling is one way to allow that without having to explicitly propagate errors everywhere. It allows an exception handler to be defined (only) at the point in a program where a proper action can be taken.

Yes, the adage generally goes, "detect errors at a low-level, handle them at a high-level". The only two answers I have at the moment are:

1. we don't really have a total language on which to conclude how prevalent and onerous this truly is,

2. monadic code structure can replicate the necessary decoupling of error generator to error handler.

I just don't know how much overhead monads really entail. No one seems to have addressed that question of mine. :-)

An Onerous Total Language

I think that Coq satisfies 1. There are probably others like ACL2, Isabelle and Agda, but I'm not very familiar with them.

I recently wrote a combinator parser in Coq which required adding exceptions. I was able to use exceptions+'dependent types' to deal with things fairly straight-forwardly and it wasn't too bad. However, I wasn't able to figure out how to encode exceptions in such a way that all exceptional situations could be defined at any point in the program code, in an "as you go" type manner. That is, I ran into "The Expression Problem". This isn't a big problem for my code, but it does make it hard to imagine a component architecture using exceptions in Coq.

In looking at the expression problem, it isn't clear to me that it can be solved in Coq or any other total strongly-typed programming language without some special ingredient. It seems that all of the solutions make use of back-doors in the type system. I'm very curious if anyone knows of a solution.


Wouter Swierstra's Data types à la carte
has a solution to the expression problem. It uses a type:

data Expr f = In (f (Expr f ))

It seems to me that this type displays contravariant type recursion, or as Coq told me: "Non-strictly positive occurrence of Expr". The reason for disallowing types like this is that bottom can sneak back in. I wasn't able to come up with an example using this type, but I'd be curious if anyone knows of one.

I think that Coq satisfies

I think that Coq satisfies 1. There are probably others like ACL2, Isabelle and Agda, but I'm not very familiar with them.

You might be right. I tend to exclude Coq because it sort of evolved from OCaml, which is not only partial, which Coq obviously addresses, but also violates capability security principles (which I think are synergistic with totality, in that some of the same problems raised by one exist in the other as well).

In looking at the expression problem, it isn't clear to me that it can be solved in Coq or any other total strongly-typed programming language without some special ingredient. It seems that all of the solutions make use of back-doors in the type system. I'm very curious if anyone knows of a solution.

You might be right. OCaml's solution with polymorphic variants requires a form of unrestricted recursion. I wouldn't argue for full totality, particularly with regards to recursion, but I've come to strong believe that totality minus recursion at the library level is a must.

Signals

Is there something which helps us get at all these issues more rigorously and sanely?

Your answer is 'signals'.

All the exception type mechanisms (as well as all control flow mechanisms) can be modelled with signals.

Which signals?

Signal being one of those words (along with "process" and "message") which can mean widely different things depending on which computer science context you are in. I'm assuming that you don't mean the OS-generated asynchronous notification mechanism found in Unix and similar operating systems; nor signals-as-events as found in frameworks like Qt.

In fact, if you'll pardon my ignorance :), I'm not entirely sure what you mean. :)

Exceptions are Continuations on the Stack

Exceptions are more than just an exceptional return value, or signal, or message. An exception is also a pointer to an exception handler which is on the thread’s stack.

When an exception is just a return value, the caller must test to see if the return is exceptional. To avoiding the obviously painful tests for exceptionality after every method call, one can use error state objects that ignore further method calls, or use syntactic constructions that allow the programmer to see a simplified view (like syntactic sugar try catch blocks). This may look simpler, but it is inefficient.

You want to keep an active list of possible caught exceptions (and their respective handler) at runtime. When an exception is thrown, the lookup is done and control is given to the correct exception handler. There are no return value checks being done. Of course, you must maintain this list, but you save by removing all the return-value checks and shortening your code overall.

Similarly, state machines should be coded as lookup tables with a simple state transition routine, as opposed to directly in the processor’s native instruction set. This is because the table and code take less space than just code, and runs faster. I personally found it interesting that “optimizing” with constant propagation and loop unrolling could make your code run slower.

So, by maintaining a little runtime state, the size of the code can be reduced considerably and run faster as a result. I can not imagine the full panoply of good uses that could come of this.

For example, Null pointer handling could be done the same way as exceptions, redirecting control directly to null pointer handler instead of having explicit checks in code. This could give the developer the rigour of NOT-NULL constraints with the additional benefit of context sensitive handing of such an error. Probably some languages already does this, converting null-assigns to exceptions.

I think efficient exceptions

I think efficient exceptions leads to overuse of exceptions. We should instead strive for better interfaces which ideally wouldn't throw *any* exceptions. For instance, instead of throwing null pointer exceptions, just eliminate the possibility of null pointers entirely!

Inefficient exceptions or returning errors (as a sum+pattern matching) encourages total interfaces which inevitably improves software reliability.

Elimination of null pointers isn't hard...

...and many (including me) feel that unavoidable-null is a big problem.

On the other hand, NULL is a cheap hacky way of implementing the Maybe monad WRT pointer/reference types. It has the advantage that null-trapping is frequently free at runtime, at least until you try and dereference one. It has the disadvantage that the languages with null pointers make it hard to avoid--and frequently use nulls for things other than to indicate failure (such as null=default, null=no change; see also this c2 page for a list of ways that nulls are (ab)used).

It is possible (though it requires a lot of typing) to do bottom-propogation with NULLs. (I.e. write functions from pointers to pointers that return NULL when passed NULL).

That said--there is much to be said for writing routines which cannot return failure.

Ada 2005:function Lookup (T

Ada 2005:

function Lookup (T : Table) return not null Ref_Element;

Safer and more efficient.

Defining things

Another part of the problem is that different folks have different priorities. E.g. some folks like being able to string along computations and have failure gracefully flow through, whereas others see that has rather unnecessarily inefficient.

Has anybody ever made a big-ass Karnaugh map of all the feature requests around error handling? :-)