سؤال

Common Lisp allows exception handling through conditions and restarts. In rough terms, when a function throws an exception, the "catcher" can decide how/whether the "thrower" should proceed. Does Prolog offer a similar system? If not, could one be built on top of existing predicates for walking and examining the call stack?

هل كانت مفيدة؟

المحلول

The ISO/IEC standard of Prolog provides only a very rudimentary exception and error handling mechanism which is - more or less - comparable to what Java offers and far away from Common Lisp's rich mechanism, but there are still some points worth noting. In particular, beside the actual signalling and handling mechanism, many systems provide a mechanism similar to unwind-protect. That is, a way to ensure that a goal will be executed, even in the presence of otherwise unhandled signals.

ISO throw/1, catch/3

An exception is raised/thrown with throw(Term). First a copy of Term is created with copy_term/2 lets call it Termcopy and then this new copy is used to search for a corresponding catch(Goal, Pattern, Handler) whose second argument unifies with Termcopy. When Handler is executed, all unifications caused by Goal are undone. So there is no way for the Handler to access the substitutions present when throw/1 is executed. And there is no way to continue at the place where the throw/1 was executed.

Errors of built-in predicates are signaled by executing throw(error(Error_term, Imp_def)) where Error_term corresponds to one of ISO's error classes and Imp_def may provide implementation defined extra information (like source file, line number etc).

There are many cases where handling an error locally would be of great benefit but it is deemed by many implementors to be too complex to implement.

The additional effort to make a Prolog processor handle each and every error locally is quite considerable and is much larger than in Common Lisp or other programming languages. This is due to the very nature of unification in Prolog. The local handling of an error would require to undo unifications performed during the execution of the built-in: An implementor has thus two possibilities to implement this:

  • create a "choice point" at the time of invoking a built-in predicate, this would incur a lot of additional overhead, both for creating this choice point and for "trailing" subsequent bindings
  • go through each and every built-in predicate manually and decide on a case-by-case basis how to handle errors — while this is the most efficient in terms of runtime overheads, this is also the most costly and error-prone approach

Similar complexities are caused by exploiting WAM registers within built-ins. Again, one has the choice between a slow system or one with significant implementation overhead.

exception_handler/3

Many systems, however, provide internally better mechanisms, but few offer them consistently to the programmer. IF/Prolog provides exception_handler/3 which has the same arguments as catch/3 but handles the error or exception locally:

[user] ?- catch((arg(a,f(1),_); Z=ok), error(type_error(_,_),_), fail).

no

[user] ?- exception_handler((arg(a,f(1),_); Z=ok), error(type_error(_,_),_), fail).

Z       = ok

yes

setup_call_cleanup/3

This built-in offered by quite a few systems. It is very similar to unwind-protect but requires some additional complexity due to Prolog's backtracking mechanism. See its current definition.


All these mechanisms need to be provided by the system implementor, they cannot be built on top of ISO Prolog.

نصائح أخرى

ISO prolog defines these predicates:

  • throw/1 which throws an exception. The argument is the exception to be thrown (any term)
  • catch/3 which executes a goal and catches certain exceptions in which case it executes the exception handler. First argument is the goal to be called, second argument is the exception template (if exception thrown by throw/1 unifies with this template the handler goal is executed), and the third argument is the handler goal is executed.

Example usage:

test:-
  catch(my_goal, my_exception(Args), (write(exception(Args)), nl)).

my_goal:-
  throw(my_exception(test)).

Regarding your note "If not, could one be built on top of existing predicates for walking and examining the call stack?" i don't think there is a general way to do this. Maybe look at the documentation of the prolog system you are using to see if there is some way to walk through the stack.

As false mentioned in his answer, ISO Prolog doesn't allow this. However, some experimentation shows that SWI-Prolog has provided a mechanism on which conditions and restarts could be built. A very rough proof of concept follows.

The "catcher" invokes restart/2 to call a goal and supplies a predicate for choosing among available restarts should a condition be raised. The "thrower" invokes signal_condition/2. The first argument is the condition to raise. The second argument will be bound to a chosen restart. If no restart is chosen, the condition becomes an exception.

restart(Goal, _) :-  % signal condition finds this predicate in the call stack
    call(Goal).

signal_condition(Condition, Restart) :-
    prolog_current_frame(Frame),
    prolog_frame_attribute(Frame, parent, Parent),
    signal_handler(Parent, Condition, Restart).

signal_handler(Frame, Condition, Restart) :-
    ( prolog_frame_attribute(Frame, goal, restart(_, Handler)),
        call(Handler, Condition, Restart)
    -> true
    ; prolog_frame_attribute(Frame, parent, Parent)
    -> signal_handler(Parent, Condition, Restart)
    ; throw(Condition)  % reached top of call stack
    ).

You can use hypothetical reasoning, to implement what you want. Lets say a Prolog system that allows hypothetical reasoning supports the following inference rule:

G, A |- B
----------- (Right ->)
G |- A -> B

There are some Prolog systems that support this, for example lambda Prolog. You can now use hypothetical reasoning to implement for example restart/2 and signal_condition/3. Assume the hypothetical reasoning is done via (-:)/2, we could then have:

restart(Goal,Handler) :- 
   (handler(Handler) -: Goal).

signal_condition(Condition, Restart) :- 
   handler(Handler), call(Handler,Condition,Restart), !.
signal_condition(Condition, _) :- 
   throw(Condition).

The solution will not for nothing traverse the whole stack trace, but directly query for a handler. But it begs the question whether I need a special Prolog or whether I can do hypothetical reasoning by myself. As a first approximation the (-:)/2 can be implemented as follows:

(Clause -: Goal) :- assume(Clause), Goal, retire(Clause).

assume(Clause) :- asserta(Clause).
assume(Clause) :- once(retact(Clause)).

retire(Clause) :- once(retract(Clause)).
retire(Clause) :- asserta(Clause).

But the above will not work correctly if Goal issues a cut or an exception. So a better solution available for example in Jekejeke Minlog 0.6 would be:

(Clause -: Goal) :- compile(Clause, Ref), assume_ref(Ref), Goal, retire_ref(Ref).

assume_ref(Ref) :- sys_atomic((recorda(Ref), sys_unbind(erase(Ref)))).

retire_ref(Ref) :- sys_atomic((erase(Ref), sys_unbind(recorda(Ref)))).

The sys_unbind/1 predicate schedules an undo goal on the binding list. It corresponds to the undo/1 from SICStus. The binding list is resilient to cuts. The sys_atomic/1 assures that the undo goal is always schedule, even if an external signal happens during the execution, such as for example an end-user issued abort. It corresponds to how for example the first argument of setup_call_cleanup/3 is handled.

The advantage of using clause references here is that the clause is only compiled once, even if backtracking happens between the goal and the continuation after the (-:)/2. But otherwise the solution is most likely slower than putting a goal on the stack trace via calling it. But one could imagine further refinements of a Prolog system, for example (-:)/2 as a primitive and appropriate compile techniques.

Bye

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top