调试Prolog需要一些不同的技能,所以我们需要花费更多时间来了解。
首先,让我们注意到两个样本查询的有趣之处。第一个成功了,这是应该的;第二个应该失败,但却进入了循环。这个细节是一个线索:它表明我们正在尝试处理一个假设情况。这是在使用其他语言后使用Prolog时常见的错误。在Prolog中,通常只需要明确成功的情况,让失败通过失败的统一来发生即可。
调试Prolog的标准工具是trace/0
。思路是,您激活跟踪模式,然后运行查询,如下所示:
?- trace, is_middle(1,[2,2,3]).
< p >
trace/0
存在的问题在于理解它需要一些努力。每行都以下列四个动词之一开头:call(调用)、exit(退出)、redo(重做)或fail(失败)。然后有一个数字,表示调用的嵌套级别。call和redo告诉你正在进入一次计算;exit和fail告诉你一次计算正在停止,嵌套级别即将减少。Call/exit是正常情况,fail/redo是Prolog特殊之处,即非确定性。通常,无限循环看起来像是一些有意义的工作(或可能不是)的前缀,后面是不断重复输出来自
trace
的块。我们在此处看到了这种情况。前缀:
Call: (8) is_middle(1, [2, 2, 3]) ? creep
Call: (9) middle([2, 2, 3], _G1194) ? creep
Call: (10) remove_first_and_last([2, 2, 3], _G1194) ? creep
Call: (11) remove_first([2, 2, 3], _G1194) ? creep
Exit: (11) remove_first([2, 2, 3], [2, 3]) ? creep
Call: (11) remove_last([2, 3], _G1197) ? creep
Call: (12) remove_last([3], _G1190) ? creep
Exit: (12) remove_last([3], []) ? creep
Exit: (11) remove_last([2, 3], [2]) ? creep
Exit: (10) remove_first_and_last([2, 2, 3], [2]) ? creep
重复块:
Call: (10) middle([2], _G1200) ? creep
Exit: (10) middle([2], [2]) ? creep
Exit: (9) middle([2, 2, 3], [2]) ? creep
Call: (9) member(1, [2]) ? creep
Call: (10) member(1, []) ? creep
Fail: (10) member(1, []) ? creep
Fail: (9) member(1, [2]) ? creep
Redo: (10) middle([2], _G1200) ? creep
Call: (11) remove_first_and_last([2], _G1200) ? creep
Exit: (11) remove_first_and_last([2], [2]) ? creep
现在你可以看到,通过这个查询,触发不良行为将变得更加容易:
[trace] ?- is_middle(2,[3]).
Call: (7) is_middle(2, [3]) ? creep
Call: (8) middle([3], _G398) ? creep
Exit: (8) middle([3], [3]) ? creep
Call: (8) member(2, [3]) ? creep
Call: (9) member(2, []) ? creep
Fail: (9) member(2, []) ? creep
Fail: (8) member(2, [3]) ? creep
Redo: (8) middle([3], _G398) ? creep
Call: (9) remove_first_and_last([3], _G398) ? creep
Exit: (9) remove_first_and_last([3], [3]) ? creep
Call: (9) middle([3], _G401) ? creep
Exit: (9) middle([3], [3]) ? creep
Exit: (8) middle([3], [3]) ? creep
Call: (8) member(2, [3]) ? creep
Call: (9) member(2, []) ? creep
Fail: (9) member(2, []) ? creep
Fail: (8) member(2, [3]) ? creep
Redo: (9) middle([3], _G401) ? creep
现在可以清楚地看到问题与
middle/2
、
remove_first_and_last/2
和
member/2
相互作用有关。你对
member/2
的定义恰好是标准定义,因此可能不应受到责备。有趣的是,你让
middle/2
调用了它自己和
remove_first_and_last/2
。而且
middle/2
和
remove_first_and_last/2
都有一个相同的子句:
m([X], [X])
。
这种情况是无限递归的一个极好的生成器,因为
middle/2
在第二个子句中所做的第一件事情正是它在自己的第一个子句中尝试并失败的事情。因此,它会发现自己在第二个子句中以与先前失败调用自身时完全相同的状态进入了递归调用。
解决方案是查看
remove_first_and_last/2
并意识到你在那里的第一个子句实际上没有删除第一个和最后一个元素。移除
remove_first_and_last([X], [X])
子句可修复代码:
[trace] ?- is_middle(2,[3]).
Call: (7) is_middle(2, [3]) ? creep
Call: (8) middle([3], _G398) ? creep
Exit: (8) middle([3], [3]) ? creep
Call: (8) member(2, [3]) ? creep
Call: (9) member(2, []) ? creep
Fail: (9) member(2, []) ? creep
Fail: (8) member(2, [3]) ? creep
Redo: (8) middle([3], _G398) ? creep
Call: (9) remove_first_and_last([3], _G398) ? creep
Call: (10) remove_first([3], _G398) ? creep
Fail: (10) remove_first([3], _G398) ? creep
Fail: (9) remove_first_and_last([3], _G398) ? creep
Fail: (8) middle([3], _G398) ? creep
Fail: (7) is_middle(2, [3]) ? creep
false.
你的这两个测试现在也都可以工作了:
?- is_middle(1,[2,1,3]).
true.
?- is_middle(1,[2,2,3]).
false.
我认为你在这里添加基本情况是出于责任感,但实际上,如果你有一个元素的列表,在任何情况下它都不应该与
remove_first_and_last/2
一起统一。 这与明确处理Prolog中的错误情况非常相似,这往往会干扰机器的工作。
现在,缺少的一件事是,你想如何处理偶数长度的列表? 无论您是否更改,现在您所拥有的内容都不能处理偶数长度的列表。 偶数长度的列表没有中间元素; 这是您想要的吗? 我怀疑不是,因为
is_middle/2
中出现了
member/2
。
is_middle/2
的评论
你所拥有的东西可以重构如下:
is_middle(X, In) :- middle(In, [X]).
member/2
的使用并没有起到任何作用,因为 middle/2
永远不可能在其第二个参数中生成非单例列表。但是,如果它确实生成了长度为偶数的列表,那么这将是有利可图的。你甚至可以通过向 middle/2
添加第三个子句来使代码按照这种方式工作:
middle([X,Y], [X,Y]).
现在看看
middle/2
在处理偶数长度的列表时如何工作:
?- middle([2,1,3,4], X).
X = [1, 3] ;
false.
现在切割过程可能会让你陷入麻烦。例如,1和3都是is_middle/2
:
?- is_middle(1, [2,1,3,4]).
true.
?- is_middle(3, [2,1,3,4]).
true.
不幸的是,如果您要求获取中间元素,您只会得到1
:
?- is_middle(X, [2,1,3,4]).
X = 1.
3
发生了什么?你的切割操作阻止了它的生成。我不确定为什么要进行切割操作。我认为你可能是为了控制无限递归而进行切割操作,但这并没有帮助到你,因此请将其删除。
随意添加切割操作进行调试通常不是一个好主意。使用Ulrich Neumerkel的失败片段(failure slice)方法是一个更好的选择(详见这篇论文或者搜索相关标签以获取更多信息)。
DCG奖励
你可以将remove_first_and_last/2
重写为DCG规则:
remove_first_and_last --> remove_first, remove_last.
很酷,对吧? :) 那是因为你在该规则中进行的输入/输出线程与DCG规则完全相同。
更改摘要
remove_first_and_last(In, Out) :-
remove_first(In, Out1),
remove_last(Out1, Out).
middle([X], [X]).
middle([X,Y], [X,Y]).
middle(In, X) :-
remove_first_and_last(In, Out),
middle(Out, X).
is_middle(1,[2,2,3])
时,它会调用middle([2,3,3], Out)
。当你调用它时会发生什么?而middle([2,3,3], Out)
将调用remove_first_and_last([2,3,3], Out)
,那么当你调用它时会发生什么?等等... - lurkertrace/0
很有用,例如trace, is_middle(1, [2,2,3])
,因为您可以通过它交互式地看到循环的弧线。 - Daniel Lyonsis_middle(3, [1,2,3,4])
应该是真的吗? - lurkeris_middle(2, [1,2,3,4])
也应该是真的吗? - false