manifesto
Code should be bug-free.
Design by Contract
is one path toward that.
What should I do with the fatal errors? Make the program crash?
Yes.
Or make it do the Right Thing, if you prefer.
(The current implementation just ignores the error and stumbles forward.)
total function
A function is specified to accept inputs and
produce some result: return value and/or side effects.
The inputs can be explicit in the signature,
they can also be global variables,
filesystem fullness, wallclock time,
PRNG
state, and so on.
Some functions are
total --
they will always produce a correct result, for any input.
Here is one, in python:
def increment(n: int) -> int:
return n + 1
Assuming we can malloc(), and the power is on,
this function pretty much always returns what you expect.
Here is a similar function in C:
uchar increment(uchar n) {
return n + 1;
}
Calling increment(254) does the Right Thing.
But probably the caller is being too adventurous
upon supplying an unsigned arg of 255.
For modulo operations this is well defined,
but if the contract is to "return a bigger number",
then this is a partial function.
If this were C++ we might choose to throw an exception for that.
The familiar sqrt() function offers a similar example.
If we rule out NaN and complex / imaginary numbers,
then caller should avoid offering a negative argument.
This is called a pre-condition on acceptable inputs.
If we do see negative input,
it is appropriate for sqrt to immediately raise a fatal error
so caller will fix their broken code.
post-condition
The contract for the root finder looks like this:
"Give me a non-negative real.
I promise to hand back a real which, when squared,
equals the original argument."
(We are in FP-land, here, so we finesse "equals"
to mean "low relative error", typically specified
by some epsilon parameter.)
Now, in a case like sqrt(-3), there is simply
no way to fulfill that contract.
What should the root finder do?
Return a sentinel like 99?
(Nevermind that it's indistinguishable from sqrt(9801).)
Maybe "better" sentinel values happen to be
available, like -99?
Sure, that's one possibility that folks have tried.
It makes the contract more complex,
and some callers will forget to check for that condition.
Or, we could throw up our hands!
The much better alternative, when faced with an "impossible" contract,
is to give up, with raise.
Did we return some float value?
No.
We returned nothing, the computation didn't happen.
Throwing an exception interrupts the flow so caller
won't accidentally trust some arbitrary sentinel value
as being "correct".
It might be a fatal exception, which halts with a stack trace.
Or the caller might have anticipated this situation,
and have some strategy for coping with it.
Caller might try an alternative call (flip the sign),
or pause while it asks a human for help,
or re-raise so its higher level caller
is aware of the situation.
Make the program crash?
Yes. Or no.
It's not your responsibility.
A pre-condition wasn't satisfied or you
can't make your post-condition true for whatever reason,
so you bail out and make the caller aware of it.
What the caller does is up to him, it's not your job.