调试 Prolog 需要一些不同的技巧,所以让我们走很长的路。
首先,让我们注意您的两个示例查询的一些有趣之处。第一个成功了,它应该;第二个应该失败,但它会循环。这个花絮是一个线索:它表明我们正在尝试处理一个虚假案件。这是在其他语言之后使用 Prolog 的人们的常见错误。在 Prolog 中,明确说明成功案例并让失败通过失败的统一发生就足够了。
调试 Prolog 的标准工具是trace/0。这个想法是,您激活跟踪模式,然后运行您的查询,如下所示:
?- trace, is_middle(1,[2,2,3]).
trace/0 的问题在于它可能需要一些努力才能了解它发生了什么。每行都以以下四个动词之一开始:调用、退出、重做或失败。然后有一个数字表示调用的嵌套级别。 call 和 redo 动词告诉你你正在进入一个计算; exit 和 fail 告诉您计算正在停止并且嵌套级别即将降低。调用/退出是正常情况,失败/重做是 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 在它的 second 子句中所做的第一件事正是它刚刚尝试做的事情,但它自己的 却失败了第一个子句。因此,它可以发现自己在第二个子句中进入了递归调用,与之前对其自身的失败调用中的状态完全相同。
解决方案是查看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 显式处理错误情况非常相似,后者往往会干扰机器的工作。
现在,缺少的一件事是,您想如何处理偶数长度的列表?无论有没有我的改变,你现在所拥有的都不会。偶数列表没有中间元素;那是你的意图吗?我怀疑不是,因为member/2 在is_middle/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 的 故障切片 方法(请参阅 this paper 或 search the tag 了解更多信息)。
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).