On 03/10/2014 18:40, Sean Kelly wrote:
I finally realized what's been bugging me about thew program logic
error, airplane vs. server discussion, and rather than have it lost in
the other thread I thought I'd start a new one. The actual problem
driving these discussions is that the vocabulary we're using to describe
error conditions is too limited. We currently have a binary condition.
Either something is a temporary environmental condition, detected at
run-time, which may disappear simply by retrying the operation, or it is
a programming logic error which always indicates utter, irrecoverable
failure.
Setting aside exceptions for the moment, one thing I've realized about
errors is that in most cases, an API has no idea how important its
proper function is to the application writer. If a programmer passes
out of range arguments to a mathematical function, his logic may be
faulty, but *we have no idea what this means to him*. The confusion
about whether the parameters to a library constitute user input is
ultimately the result of this exact problem--since we have no idea of
the importance of our library in each application, we cannot dictate how
the application should react to failures within the library.
Spot on, I pretty much agree with everything above, but up to this point
only.
After that, not so much.
A contract has preconditions and postconditions to validate different
types of errors. Preconditions validate user input (caller error), and
postconditions validate resulting state (callee error). If nothing
else, proper communication regarding which type of error occurred is
crucial. A precondition error suggests a logic error in the
application, and a postcondition error suggests a logic error in the
function. The function writer is in the best position to know the
implications of a postcondition failure, but has no idea what the
implications of a precondition failure might be. So it's reasonable to
assert that not only the type of contract error is important to know
what to fix, but also to know how to react to the problem.
Another issue is what the error tells us about the locality of the
failure. A precondition indicates that the failure simply occurred
sometime before the precondition was called, while a postcondition
indicates that the failure occurred within the processing of the
function. Invariant failures might indicate either, which leads me to
think that they are too coarse-grained to be of much use. In general, I
would rather know whether the data integrity problem was preexisting or
whether it occurred as the result of my function. Simply knowing that
it exists at all is better than nothing, but since we already have the
facility for a more detailed diagnosis, why use invariants?
"A precondition error suggests a logic error in the application, and a
postcondition error suggests a logic error in the function."
Suggests yes, but it doesn't guarantee. A postcondition error (or
invariant failure) in a component (imagine a library) could well be
triggered not due to a bug in that component, but a bug in the use of
that component.
Only if the component properly defines all the preconditions for all its
functions that case should not happen. But in practice we know that
software is not perfect, and a lot of preconditions may not be
explicitly defined.
Without running on too long, I think the proper response to this issue
is to create a third subtype of Throwable to indicate contract
violations, further differentiating between pre and postcondition
failures. So we use Exception to represent (environmental) errors which
may disappear simply from retrying the operation (and weirdly, out of
memory falls into this category, though @nothrow precludes
recategorization), Error to represent, basically, the things we want to
be allowable in @nothrow code, and ContractFailure (with children:
PreconditionFailure, PostconditionFailure, and InvariantFailure) to
indicate contract violations. This gets them out from under the
Exception umbrella in terms of having them accidentally discarded, and
gives the programmer the facility to handle them explicitly. I'm not
entirely sure how they should operate with respect to @nothrow, but am
leaning towards saying that they should be allowable just like Error.
Even if we could correctly differentiate between precondition failures
and postcondition ones, what would that gives us of use?
I think in practice when some code hits an error in some component that
it uses, knowing whether it is a precondition failure (bug in the code
using the component), or a postcondition (bug in the used component
itself), it may actually not tell us much about how much of the program
has been affected, ie, which fault domain is broken.
I think the default behavior should be simply the clean throw of an
Exception when an assertion fails. If there is a performance issue with
this, and we want to crash the program immediately when an assertion
fails, then that should be an option too. However this behavior should
be configurable per library/component, not globally for the whole
program, that is too coarse. Also, it should be configurable *in the
code* itself, not at compile time. For some components it should even be
possible to use the component with hard-stop assertion failure behavior
in some places in the program, and with clean exceptions in other places
of the *same program*.
--
Bruno Medeiros
https://twitter.com/brunodomedeiros