5

Common Lisp 允许通过条件和重新启动来处理异常。粗略地说,当一个函数抛出异常时,“捕捉者”可以决定“抛出者”应该如何/是否应该继续。Prolog 是否提供类似的系统?如果没有,是否可以在现有谓词之上构建一个用于遍历和检查调用堆栈的谓词?

4

4 回答 4

4

Prolog的ISO/IEC 标准只提供了一个非常基本的异常和错误处理机制,它或多或少地与 Java 提供的相当,与 Common Lisp 的丰富机制相去甚远,但仍有一些值得注意的地方。特别是,除了实际的信令和处理机制之外,许多系统都提供了类似于unwind-protect. 也就是说,一种确保目标将被执行的方法,即使存在其他未处理的信号。

ISO投掷/1,接球/3

使用 引发/抛出异常throw(Term)Term首先创建一个副本,copy_term/2让我们调用它Termcopy,然后这个新副本用于搜索catch(Goal, Pattern, Handler)其第二个参数与 一致的对应项Termcopy。当Handler被执行时,所有由 引起的统一Goal都被撤消。因此,在执行Handler时无法访问存在的替换throw/1。并且没有办法在throw/1执行的地方继续。

内置谓词的错误通过执行对应于ISO 错误类throw(error(Error_term, Imp_def))之一的where发出信号,并且可能提供实现定义的额外信息(如源文件、行号等)。Error_termImp_def

在许多情况下,在本地处理错误会大有裨益,但许多实现者认为它过于复杂而无法实现。

使 Prolog 处理器在本地处理每个错误的额外工作是相当可观的,并且比 Common Lisp 或其他编程语言大得多。这是由于 Prolog 中统一的本质。错误的本地处理将需要撤消在内置执行期间执行的统一:因此,实现者有两种可能性来实现这一点:

  • 在调用内置谓词时创建一个“选择点”,这会产生很多额外的开销,无论是创建这个选择点还是“尾随”后续绑定
  • 手动检查每个内置谓词,并根据具体情况决定如何处理错误——虽然这在运行时开销方面是最有效的,但也是最昂贵和最容易出错的方法

类似的复杂性是由利用内置的 WAM 寄存器引起的。同样,人们可以在缓慢的系统或具有显着实现开销的系统之间进行选择。

异常处理程序/3

然而,许多系统在内部提供了更好的机制,但很少有人始终如一地向程序员提供它们。IF/Prolog 提供exception_handler/3了与它具有相同参数catch/3但在本地处理错误或异常的方法:

[用户] ?- catch((arg(a,f(1),_); Z=ok), 错误(type_error(_,_),_), 失败)。

不

[用户] ?- exception_handler((arg(a,f(1),_); Z=ok), error(type_error(_,_),_), 失败)。

Z = 好的

是的

setup_call_cleanup/3

很多系统都提供了这个内置功能。unwind-protect由于 Prolog 的回溯机制,它非常相似,但需要一些额外的复杂性。请参阅其当前定义


所有这些机制都需要由系统实现者提供,它们不能建立在 ISO Prolog 之上。

于 2012-09-28T15:35:22.423 回答
1

您可以使用假设推理来实现您想要的。假设允许假设推理的 Prolog 系统支持以下推理规则:

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

有一些 Prolog 系统支持这一点,例如lambda Prolog。您现在可以使用假设推理来实现例如 restart/2 和 signal_condition/3。假设假设推理是通过 (-:)/2 完成的,那么我们可以有:

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

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

该解决方案不会无缘无故地遍历整个堆栈跟踪,而是直接查询处理程序。但它引出了我是否需要一个特殊的 Prolog 或者我是否可以自己进行假设推理的问题。作为第一个近似值,(-:)/2 可以如下实现:

(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).

但如果 Goal 发出裁减或异常,上述操作将无法正常工作。因此,例如Jekejeke Minlog 0.6 中可用的更好解决方案是:

(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)))).

sys_unbind/1 谓词在绑定列表上安排撤消目标。它对应于 SICStus 的 undo/1。绑定列表对剪切具有弹性。sys_atomic/1 确保撤消目标始终是计划的,即使在执行期间发生外部信号,例如最终用户发出中止。它对应于例如 setup_call_cleanup/3 的第一个参数的处理方式。

在这里使用子句引用的好处是子句只编译一次,即使在目标和 (-:)/2 之后的延续之间发生了回溯。但除此之外,解决方案很可能比通过调用将目标放在堆栈跟踪上要慢。但是可以想象 Prolog 系统的进一步改进,例如 (-:)/2 作为一种原始且适当的编译技术。

于 2012-09-29T10:35:39.180 回答
0

ISO prolog 定义了这些谓词:

  • throw/1引发异常。参数是要抛出的异常(任何术语)
  • catch/3它执行一个目标并捕获某些异常,在这种情况下它执行异常处理程序。第一个参数是要调用的目标,第二个参数是异常模板(如果抛出的异常throw/1与该模板统一,则执行处理程序目标),第三个参数是执行处理程序目标。

示例用法:

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

my_goal:-
  throw(my_exception(test)).

关于您的注释“如果不是,是否可以在现有谓词之上构建一个用于遍历和检查调用堆栈的谓词?” 我认为没有通用的方法可以做到这一点。也许查看您正在使用的 prolog 系统的文档,看看是否有某种方法可以遍历堆栈。

于 2012-09-27T21:11:30.030 回答
0

正如他在回答中提到的错误,ISO Prolog 不允许这样做。然而,一些实验表明,SWI-Prolog 提供了一种可以建立条件和重启的机制。下面是一个非常粗略的概念证明。

“捕手”调用restart/2来调用一个目标并提供一个谓词,以便在出现条件时在可用的重新启动中进行选择。“投掷者”调用signal_condition/2. 第一个论点是提出的条件。第二个参数将绑定到选择的重新启动。如果不选择重新启动,则条件变为异常。

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
    ).
于 2012-09-28T19:13:37.713 回答