[net.lang.prolog] Error Handling

OKeefe.R.A%EDXA@sri-unix.UUCP (11/02/83)

From:  Rechard HPS (on ERCC DEC-10) <OKeefe.R.A at EDXA>

     The assert/retract question having generated more fury than
findings, I suppose it is silly of me to ask another "imperialistic"
question.  Given that the only public response to my proposed design
principle for what should be an error and what should fail was an
explicit repudiation of consistency in favour of "user convenience",
notoriously ill-defined, it's not just silly, it's downright stupid.
But the question is an important one, so I boldly ask

                What should Prolog do when it
                detects an error?

     Let's keep separate the debate over whether "X is 1+a" should
just fail (my view) or give an error message.  There are some things
where we are agreed that error messages should be produced, say
"X is 1+_" (instantiation fault) or "X is 2^300" (overflow).  And a
Prolog program may detect errors of its own.  Let's also keep syntax
errors for another debate, and concentrate on these run-time errors.

     There is no denying that the error messages in DEC-10 Prolog are
rather unhelpful.  The way that compiled code says "! mode error" and
doesn't say what predicate, and just keeps running, is a real pain.
I have put a wee bit more thought into the error messages in C-Prolog
1.4.EdAI, but not much, and the fact that several of the errors turn
tracing on is less helpful than I expected.  Chris Mellish put a lot
of thought into the error messages of PDP-11 Prolog, which try to say
clearly what the problem is and what is a likely cause.  The trouble
is that the real cause is often something else.  But let's keep the
design of error messages separate from the actions performed when an
error is detected.

     Clearly, the program cannot continue.  Making all errors fail
would be defensible, as an error is one way of failing to prove a
goal, but I think that failure should be reserved for when you KNOW
that you have failed to prove the goal, not for when the machine has
turned out to be inadequate.  (I repeat, I want "X is 1+a" to fail,
but if you want to treat it as an error, and print some sort of
message, then you shouldn't make it fail.)

     One possibility would be to copy Lisp 1.5, and to have an
errorset.  The Prolog interpreters sold by Expert Systems Ltd. have
something like this.  I don't remember the details, but it is like

        PrincipalGoal if_error AlternativeGoal

where PrincipalGoal is the goal you are really interested in, but if
an error occurs while proving it, the interpreter fails out to the
"if_error" goal and tries the alternative.  This is quite neat.  It
is easy to understand procedurally, and it gives you something very
like "recovery blocks", E.g.

        sort(Raw, Ord) :-
                experimental_sort(Raw, Ord)
                if_error merge_sort(Raw, Ord)
                if_error insertion_sort(Raw, Ord).

If an error occurs in the AlternativeGoal, it is NOT caught by the
if_error.

     This isn't bad.  It has deficiencies rather than problems.  They
provide another predicate for picking up the name of the error (error
names are numbers, I think), so your handler can do different things
for different errors, and there is another predicate for signalling
an error.  But you would in general like to control whether an error
message is printed, whether a stack trace is printed, whether the
debugger is otherwise entered, and these things have to be done when
the error is detected.  By the time you arrive in the AlternativeGoal
the stack that the programmer might want to look at has gone.

     The main problem with this scheme is that it is so simple that
people will be tempted to use it as a control structure, a means for
effecting non-local GOTOs.

     Something which should help to prevent that would be a means of
detecting in advance whether an error is likely to occur.  A good
example of that is see(File) and tell(File) when the file does not
exist.  DEC-10 Prolog has a nasty little flag "nofileerorrs" (this is
an excellent example of a "feature" put in for "user convenience")
which makes these commands fail if the file doesn't exist, in the
normal state they produce an error message.  There is a library
predicate exists(File) which tells if a file exists.  (This is built
in in C-Prolog.)  Instead of hacking the flag, you can write

        load(File) :-
                exists(File),
                see(File),
                ....
                seen.
        load(File) :-
                \+ exists(File),
                writef('Sorry, can''t open %t, nothing done.\n',
                        [File]).

     To handle input-output more generally, I suggest a new system
predicate "cant_io(Goal,Reason)" which determines why an I/O command
cannot be done, or fails if it can.  I am not aware of any predicate
like this in any existing Prolog system.  In my "imperialist" way, I
am asking whether the community think it as good an idea as I do, and
if there are improvements to it.  The following should be recognised:

        cant_io(see(File), _)           -- openable for input
        cant_io(tell(File), _)          -- openable for output
        cant_io(append(File), _)        -- openable for output at end

[C-Prolog v1.4.EdAI has append(File) as a command corresponding to
fopen(File, "a") in C.  Its omission from DEC-10 Prolog is probably
because of the general hostility of Bottoms-10..]

        cant_io(rename(Old,New), _)     -- rename/delete
        cant_io(cd(Directory), _)       -- change directory

[C-Prolog v1.2B.EdAI had cd.  I have written a "cd" program for
Bottoms-10 and it was amazingly painful.  Its omission from DEC-10
Prolog comes as no surprise to me whatsoever.]

        cant_io(save(_), _)
        cant_io(restore(_), _)
        ...
You get the picture. I am not sure whether cant_io(get0(_), _) should
exist or not, because there is already a way of detecting end of file
without using errors.  Having used Pascal and PL/I, I think it easier
to write programs that check for possible problems first than to code
error handlers, however congenial the error system.

     The best way of handling arithmetic overflow is to provide long
integers, so that it can't happen.  DEC-10 Prolog has a long integer
(and rational) package, {SU-SCORE}PS:<PROLOG>LONG.PL .  A new Prolog
implementation could quite easily do long integers in C, there is a
C library "mint" that might be usable.

     I have seen another Prolog system that handled errors another
way.  The idea was that a clause like

        p(X...Z) :-
                tests...,
                cause_error(Error).

would effectively be replaced by

        p(X...Z) :-
                $handle_error(Error, p(X...Z))

where
        $handle_error(Error, Culprit) :-
                handle_error(Error, Culprit), !.
        $handle_error(Error, Culprit) :-
                ..print message, enter debugger

This is not an accurate description, just an outline.  When I first
saw it, I was greatly taken with it.  "Neat!" I though.  But I have
since come to see its problems.

     The major problem is that there is ONE error handler for the
whole program.  An instantiation error in a user query might need one
approach, while the same error (even in the same clause) in a part of
the program might need another approach.  With a single error handler
you have to get around that by putting "I am doing X" into the data
base.  You can guess how much I like THAT.

     The fact that you can add your own clauses to the error handler
is nice.  And the fact that they are called in the context of the
error is also pleasant, because you can write E.g.

        handle_error(Error, Culprit) :-
                writef('! Error %t involving %t.\n',
                        [Error, Culprit]),
                break,
                abort.

and let the user invoke the debugger to examine the state.  There is
still a problem here, though.  You may want to fail after all, and to
do that you need something like an ancestor cut.  In general, digging
out the information you need to decide can be hard too.  And while it
is less convenient than if_error, because it is the *program* which
decides what is to happen to errors, it still tempts programmers into
using the exception-handling mechanism as a control structure.

     If you have implemented a Prolog system which has some other
method of handling errors, please tell us about it.  NOW is the time
to discuss it, while Prolog is still growing and before we all end up
with incompatible InterPrologs and MacPrologs.

     My suggestion is this.  That whenever an error is detected, by
the Prolog system or by signal_error(...) in a Prolog, an error
message should be displayed on the terminal, and the user should be
asked what s/he wants to do.  The options should include

        - enter the debugger
        - abort to the nearest top level
        - show the stack history and ask again
        - let if_error take over

and maybe more.  If the program was running compiled, entering the
debugger could be hairy, but it is something we need anyway.  If the
Prolog system had to fail out a couple of levels and retry the goal
using interpreted code that wouldn't be too bad.  In the debugger, the
user can say what s/he wants to fail, so if s/he wants the faulted
goal to fail, that's not Prolog's responsibility.  And the fact that
*every* error can be caught by the user and made to do something else
should stop programmers using error handling as a control structure.
The best candidate I know for the actual handling mechanism is Expert
Systems Ltd's "if_error".

     Oh yes, before anyone shouts at me "EVIL!  You MUSTN'T dictate
to the programmer what s/he does with error handlers.  That's
AUTHORITARIAN, and that's the worst crime in the book!"  let me point
out that in practical real-life programming, other people than the
author have to read the program, even maintain it.  I'm concerned for
those people: if they see something that looks like an error handler,
then indeed it should be something that handles errors.  If it turns
out that programmers really cannot live without a nonlocal goto such
as CATCH and THROW, the answer is to provide CATCH and THROW.  There
is no evidence yet that people do need non-local gotos in Prolog.  The
old DEC-10 Prolog compiler provided the ancestor cut (which goes part
of the way, it is certainly non-local), the current one does not, and
in four years of Prolog programming I have never missed it, though I
*have* missed nested scopes (as provided in functional languages).
The cleanest way of incorporating bizarre control structures is to
use another language such as Lisp or PopLog where they make sense.