Prolog - 无限循环

时间:2017-06-06 23:07:04

标签: prolog failure-slice

我想检查元素是否在列表中间。 我搜索中间元素然后我检查是否是列表的成员,但我得到无限循环。

我的谓词:

remove_first([_,H1|T], [H1|T]).

remove_last([_],[]).
remove_last([H|T], [H|T2]) :- remove_last(T, T2).

remove_first_and_last([X],[X]).
remove_first_and_last(In, Out) :- 
    remove_first(In, Out1),
    remove_last(Out1, Out).

middle([X], [X]).
middle(In, X) :-
    remove_first_and_last(In, Out),
    middle(Out, X).

member(X, [X|_]).
member(X, [_|T]) :- member(X, T).

is_middle(X, In) :-
    middle(In, Out),
    member(X, Out), !.

当我打电话给is_middle(1,[2,1,3])时,我才会成真。 但是当我打电话给is_middle(1,[2,2,3])时,我没有得到结果。解释器不会中断处理。

3 个答案:

答案 0 :(得分:9)

在您的情况下,您有两种选择。如你在另一个答案中所看到的那样,要么穿过痕迹文本的墙壁,要么首先尝试减少你必须理解的东西。我更喜欢后者,因为我不喜欢阅读。

但你的主要问题是这个。你说:

  

当我打电话给is_middle(1,[2,1,3])时,我就会成真。

是的,Prolog找到了一个解决方案,但它没有发现一次但是无数次。只需点击 SPACE ; 即可看到:

?- is_middle(1,[2,1,3]).
true ;
true ;
true ;
true ;
true ;
true ...

所以,您的第一个查询已经存在问题。观察此问题的最佳方法是在此查询中添加 false

?- is_middle(1,[2,1,3]), false.
** LOOPS **

现在,让我们尝试减少查询的大小。我们可以将其缩小到:

?- is_middle(1,[1]), false.
** LOOPS **

有了这个,我们现在可以查看你的程序了。在其他任何事情之前,我将删除切口。无论如何它都是错位的。

要了解实际发生的情况,我会通过在其中插入 false 来缩小您的计划范围。有了这些额外的目标,就有可能消除许多不必要的细节。而且,如果它仍在循环中,那么称为的剩余程序与我们相关。

remove_first_and_last([X],[X]).
remove_first_and_last(In, Out) :- false,
    remove_first(In, Out1),
    remove_last(Out1, Out).

middle([X], [X]) :- false.
middle(In, X) :-
    remove_first_and_last(In, Out),
    middle(Out, X), false.

is_middle(X, In) :-
    middle(In, Out), false,
    member(X, Out).

将此与您的原始程序进行比较!阅读少得多。要解决此问题,您必须修复剩余片段中的内容。我建议删除事实remove_first_and_last([X],[X]).这个事实表明某些内容已被删除,但在这种情况下,没有任何内容被删除。

直接使用的解决方案:

is_middle(E, Es) :-
   phrase(middle(E), Es).

middle(E) --> [E].
middle(E) --> [_], middle(E), [_].

它尽可能短,但它有一个小问题:它不能确定地计算答案。您可以通过查看答案来看到这一点:

?- is_middle(1, [2,1,3]).
true ;
false.

; false表示Prolog无法确定地完成计算。换句话说,剩下一些空间。你可能会想要使用剪裁。抗蚀剂!

如果你真的速度快,请选择最快的版本:

is_middle(X, Xs) :-
   Xs = [_|Cs],
   middle_el(Cs, Xs, X).

middle_el([], [X|_], X).
middle_el([_,_|Cs], [_|Xs], X) :-
   middle_el(Cs, Xs, X).

如果你想要@ DanielLyons'允许偶数长度列表具有两个中间元素的解释,看看采用上面的语法定义是多么容易。只需添加以下两条规则:

middle(E) --> [E,_].
middle(E) --> [_,E].

或者,将所有四个规则合并为一个:

middle(E) --> [E] | [E,_] | [_,E] | [_], middle(E), [_].

对于最快的解决方案,事情有点复杂......

is_middle_dl(X, Xs) :-
   Xs = [_|Cs],
   middle_el_dl(Cs, Xs, X).

middle_el_dl([], [X|_], X).
middle_el_dl([_|Cs], Xs, X) :-
   middle_el_dl2(Cs, Xs, X).

middle_el_dl2([], [A,B|_], X) :-
   ( X = A ; X = B ).
middle_el_dl2([_|Cs], [_|Xs], X) :-
   middle_el_dl(Cs, Xs, X).

要检查它,我使用SICStus,因为它为变量提供了更多可读的名称:

| ?- length(Xs, N), N mod 2 =:= 0, is_middle_dl(X, Xs).
Xs = [X,_A],
N = 2 ? ;
Xs = [_A,X],
N = 2 ? ;
Xs = [_A,X,_B,_C],
N = 4 ? ;
Xs = [_A,_B,X,_C],
N = 4 ? ;
Xs = [_A,_B,X,_C,_D,_E],
N = 6 ? ;
Xs = [_A,_B,_C,X,_D,_E],
N = 6 ? ;
Xs = [_A,_B,_C,X,_D,_E,_F,_G],
N = 8 ? ;
Xs = [_A,_B,_C,_D,X,_E,_F,_G],
N = 8 ? ;
Xs = [_A,_B,_C,_D,X,_E,_F,_G,_H,_I],
N = 10 ? ;
Xs = [_A,_B,_C,_D,_E,X,_F,_G,_H,_I],
N = 10 ? ...

答案 1 :(得分:6)

调试Prolog需要一些不同的技能,所以让我们走很长的路。

首先,让我们注意一下您的两个示例查询的一些有趣内容。第一个成功,它应该成功;第二个应该失败,而是循环。这个消息是一个线索:它表明我们正试图处理一个错误的案例。这是使用Prolog后其他语言的人们常犯的错误。在Prolog中,通常可以明确地说明成功案例,并且只是通过失败的统一来发生失败。

用于调试Prolog的标准工具是trace/0。我们的想法是,您激活跟踪模式,然后运行您的查询,如下所示:

?- trace,  is_middle(1,[2,2,3]).

trace/0的问题在于它可能需要一些努力来了解它正在发生的事情。每行以这四个动词中的一个开头:call,exit,redo或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/2remove_first_and_last/2member/2的相互作用有关。您对member/2的定义完全是标准定义,因此可能不应该受到指责。现在,有趣的是,您middle/2同时调用了自己和remove_first_and_last/2middle/2remove_first_and_last/2都有相同的条款:m([X], [X])

这种事情是一个无限递归的伟大生成器,因为{em>第二个子句中的middle/2首先做的就是它刚刚尝试做的事情并且失败了它自己的< em> first 子句。因此,它可以发现自己在第二个子句中进入递归调用,其中与先前对自身的失败调用中的状态完全相同

解决方案是查看remove_first_and_last/2并意识到你的第一个子句 not 实际上删除了第一个和最后一个元素。删除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 papersearch 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).

答案 2 :(得分:6)

is_middle(Item,List) :-
    append(Left,[Item|Right],List),
    length(Left,X),
    length(Right,X).

复杂的解决方案是不好的解决方案,我的朋友。

?- is_middle(X,[1,2,3,4,5]).
X = 3 ;
false.

完全可逆的谓词:

?- is_middle(3,L).
L = [3] ;
L = [_G13, 3, _G19] ;
L = [_G13, _G19, 3, _G25, _G28] ;
相关问题