【发布时间】:2012-09-27 20:05:12
【问题描述】:
Common Lisp 允许通过conditions and restarts 处理异常。粗略地说,当一个函数抛出异常时,“捕捉者”可以决定“抛出者”应该如何/是否应该继续。 Prolog 是否提供类似的系统?如果没有,是否可以在现有谓词之上构建一个用于遍历和检查调用堆栈的谓词?
【问题讨论】:
标签: prolog iso-prolog
Common Lisp 允许通过conditions and restarts 处理异常。粗略地说,当一个函数抛出异常时,“捕捉者”可以决定“抛出者”应该如何/是否应该继续。 Prolog 是否提供类似的系统?如果没有,是否可以在现有谓词之上构建一个用于遍历和检查调用堆栈的谓词?
【问题讨论】:
标签: prolog iso-prolog
ISO/IEC standard of Prolog 仅提供了一个非常基本的异常和错误处理机制,它或多或少与 Java 提供的相当,与 Common Lisp 的丰富机制相去甚远,但仍有一些值得注意的地方。特别是,除了实际的信令和处理机制之外,许多系统都提供了类似于unwind-protect 的机制。也就是说,即使存在其他未处理的信号,也能确保目标得以执行。
使用throw(Term) 引发/抛出异常。首先使用copy_term/2 创建Term 的副本,我们将其命名为Termcopy,然后使用此新副本搜索对应的catch(Goal, Pattern, Handler),其第二个参数与Termcopy 统一。当Handler 被执行时,由Goal 引起的所有统一都被撤消。因此,Handler 无法访问在执行throw/1 时存在的替换。而且在throw/1被执行的地方也没有办法继续下去。
内置谓词的错误通过执行throw(error(Error_term, Imp_def)) 发出信号,其中Error_term 对应于ISO's error classes 和Imp_def 之一可能提供实现定义的额外信息(如源文件、行号等)。
在许多情况下,在本地处理错误会大有裨益,但许多实施者认为它太复杂而无法实施。
让 Prolog 处理器在本地处理每个错误的额外工作是相当可观的,并且比 Common Lisp 或其他编程语言大得多。这是由于 Prolog 中统一的本质。错误的本地处理将需要撤消在内置执行期间执行的统一:因此实现者有两种可能性来实现:
利用内置程序中的 WAM 寄存器会导致类似的复杂性。同样,人们可以在速度较慢的系统或实施开销很大的系统之间进行选择。
然而,许多系统在内部提供了更好的机制,但很少有人始终如一地向程序员提供它们。 IF/Prolog 提供了exception_handler/3,其参数与catch/3 相同,但在本地处理错误或异常:
很多系统都提供了这个内置功能。它与unwind-protect 非常相似,但由于 Prolog 的回溯机制,它需要一些额外的复杂性。请参阅其current definition。
所有这些机制都需要由系统实现者提供,它们不能建立在 ISO Prolog 之上。
【讨论】:
setup_call_cleanup/3 与 Common Lisp 中的unwind-protect 具有(或多或少)相同的作用:它是处理错误的系统的一部分,因此需要在该上下文中提及。
您可以使用假设推理来实现您想要的。让我们说 允许假设推理的 Prolog 系统支持以下内容 推理规则:
G, A |- B
----------- (Right ->)
G |- A -> B
有一些 Prolog 系统支持这一点,例如 lambda Prolog。 您现在可以使用假设推理来实现例如 restart/2 和信号条件/ 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 作为原始和适当的编译 技术。
【讨论】:
ISO prolog 定义了这些谓词:
throw/1 引发异常。参数是要抛出的异常(任何术语)catch/3 执行一个目标并捕获某些异常,在这种情况下它执行异常处理程序。第一个参数是要调用的目标,第二个参数是异常模板(如果throw/1抛出的异常与该模板统一执行handler目标),第三个参数是执行handler目标。示例用法:
test:-
catch(my_goal, my_exception(Args), (write(exception(Args)), nl)).
my_goal:-
throw(my_exception(test)).
关于您的注释“如果没有,是否可以在现有谓词之上构建一个用于遍历和检查调用堆栈的谓词?” 我认为没有一种通用的方法可以做到这一点。也许查看您正在使用的 prolog 系统的文档,看看是否有某种方法可以遍历堆栈。
【讨论】:
正如他在回答中提到的错误,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
).
【讨论】: