Common Lisp 允许通过条件和重新启动来处理异常。粗略地说,当一个函数抛出异常时,“捕捉者”可以决定“抛出者”应该如何/是否应该继续。Prolog 是否提供类似的系统?如果没有,是否可以在现有谓词之上构建一个用于遍历和检查调用堆栈的谓词?
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_term
Imp_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 之上。
您可以使用假设推理来实现您想要的。假设允许假设推理的 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 作为一种原始且适当的编译技术。
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 系统的文档,看看是否有某种方法可以遍历堆栈。
正如他在回答中提到的错误,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
).