Prolog中的递归如何从内部起作用。一个例子

时间:2013-01-22 15:59:29

标签: prolog prolog-dif failure-slice

我在这里有一个小脚本,可以将元素列表转换为集合。例如列表[1,1,2,3] - >设置[1,2,3]。有人可以一步一步地向我解释,这些程序中发生了什么?你能用我的[1,1,2,3] - > [1,2,3]例子?

list_to_set([],[]).

list_to_set([A|X],[A|Y]):-
   list_to_set(X,Y),
    \+member(A,Y).

list_to_set([A|X],Y):-
   list_to_set(X,Y),
   member(A,Y).

4 个答案:

答案 0 :(得分:4)

如果你看一个具体的例子,很快就会被埋没在许多无关紧要的细节中。这么多,你将失去你的程序的重要属性。让我们看看下面的程序片段,它被称为failure slice。您可以通过在程序中添加 false 目标来获得失败。故障片与原始程序共享许多有趣的属性。例如,使用失败切片执行的目标Goal, false将永远不会使用比原始程序更多的推断。或者相反,原始程序将使用更多(或最多相同数量)的推论。所以,让我指出一个这样的片段:

list_to_set([],[]) :- false.
list_to_set([A|X],[A|Y]):-
   list_to_set(X,Y), false,
    \+member(A,Y).
list_to_set([A|X],Y):-
   list_to_set(X,Y), false,
   member(A,Y).

由于这个片段不再对具体元素感兴趣(A不再使用,member/2),我们可以将length/2用于最常用的列表。通过这种方式,我们可以观察到每个长度所需的最小推断数量,如下所示:

?- length(L, N), call_inferences((list_to_set(L,_),false;true),Inf).
N = 0, Inf = 3 ;
N = 1, Inf = 6 ;
N = 2, Inf = 12 ;
N = 3, Inf = 24 ;
N = 4, Inf = 48 ;
N = 5, Inf = 96 ;
N = 6, Inf = 192 ;
N = 7, Inf = 384 ;
N = 8, Inf = 768 ;
N = 9, Inf = 1536 ;
N = 10, Inf = 3072 ...

使用

:- meta_predicate user:call_inferences(0,-).
call_inferences( Goal, Inferences) :-
   statistics(inferences, Inferences0),
   Goal,
   statistics(inferences, Inferences1),
   Inferences is Inferences1 - Inferences0.

每个其他元素的推论数量翻倍。那就是它们成倍增长。因此,您的实施成本至少指数多的推论......无需查看具体示例。

您的计划中存在更多问题:

?- L=[A,B],list_to_set(L,S), L = [a,b].

失败,而

?-  L=[A,B], L = [a,b], list_to_set(L,S).

成功。也就是说,你的程序不再是纯粹的关系。使用maplist(dif(A),Y)代替\+ member(A,Y)

答案 1 :(得分:3)

在Prolog的某些实现中,例如GNU Prolog,您可以跟踪代码的执行情况。使用此功能,您可以逐步完成评估过程,如下所示。

$ gprolog
GNU Prolog 1.4.2
By Daniel Diaz
Copyright (C) 1999-2012 Daniel Diaz
| ?- consult('list_to_set.pro').
yes
| ?- trace.
yes
{trace}
| ?- list_to_set([1,1,2,3], X).
      1    1  Call: list_to_set([1,1,2,3],_25) ?
      2    2  Call: list_to_set([1,2,3],_57) ?
      3    3  Call: list_to_set([2,3],_83) ?
      4    4  Call: list_to_set([3],_109) ?
      5    5  Call: list_to_set([],_135) ?
      5    5  Exit: list_to_set([],[]) ?
      6    5  Call: \+member(3,[]) ?
      7    6  Call: member(3,[]) ?
      7    6  Fail: member(3,[]) ?
      6    5  Exit: \+member(3,[]) ?
      4    4  Exit: list_to_set([3],[3]) ?
      7    4  Call: \+member(2,[3]) ?
      8    5  Call: member(2,[3]) ?
      ...

有关如何解释Prolog痕迹的说明,请参阅http://remus.rutgers.edu/cs314/f2007/ryder/projects/prolog/prologTrace.html阅读痕迹部分。

答案 2 :(得分:3)

prolog程序的程序对应称为SLDNF。 查询:

 list_to_set([1,1,2,3], Result)

现在SLDNF尝试通过称为统一的过程将[1,1,2,3]与[A | X]匹配。简而言之,它检查变量是否可以实例化。 [A | X]是一个捷径,它意味着(松散地):“A是第一个元素而X是其余元素”。如果A = 1且X = [1,2,3]且Result = [1 | Y],则可以这样做。如果统一成功,那么我们在规则的主体中使用相同的替换(出现在:-)之后。您可能想知道为什么我们使用第二个条款而不是第三个或第一个条款? Prolog实际上尝试了所有这些,尝试模仿一个不确定的选择。程序上虽然它按顺序尝试它们。第一个子句不能统一(A = ?,列表中没有任何内容)。如果我们的第一次尝试失败,将在稍后检查第三个子句。让我们称之为“选择点1”,因为Prolog有这条开放的路径,如果迄今为止选择的路径都失败了,那么可以使用这条路径。

现在,SLDNF已将您的初始查询缩减为:

list_to_set([1,2,3], Y2), \+ member(1, Y2)       {Result = [1 | Y2]}

(变量可以重命名,我们这样做是为了避免与程序混淆)这就是魔术发生的地方。 SLDNF现在做的和以前一样,但输入稍有不同[1,2,3]。递归处于最佳状态。所以它试图将它与第一个子句统一,但它再次失败。它成功与第二个,同样你会得到它而不是查询的第一个元素(称为文字)我们再次有身体,变量替换X = [2,3],A = 1,Y2 = [ 1 | Y]。

现在我们有了

list_to_set([2,3], Y), \+ member (1,Y), \+ member(1, [1 | Y])  {Result = [1 | [1 | Y]]}
直到最后我才会继续。最终我们最终会检查+ member(1,[1 | Y]),这意味着“1不是头部1和尾部Y的列表的成员”。这是一个内置谓词并且会失败,因为1在该列表中(它是头部)。 Prolog将回到选择点,最终到达“选择点1”。这里的条款中的最后一个条件是“A是Y的成员”。你可以检查一下这条路径最终会成功。

对于冗长的匆忙回答感到抱歉,我希望它有所帮助。

答案 3 :(得分:1)

让我们用英语看一下,看看它是否具有直观意义。将谓词重命名为有点程序化可能会有所帮助,因此我将其称为list_set

list_set([],[]).

这表示空集对应于空列表。

list_set([A|X], [A|Y]):-
  list_set(X,Y),
  \+ member(A,Y).

这表示以A开头并继续X的列表对应于以A开头并继续Y的集合,前提是A不在Y中,Y是与X对应的集合。或者,更顺序地时尚,给定一个以A开头的列表(余数为X),我们有一个以A开头的集合(余数为Y),假设Y是对应于X的集合而A不在Y中。否定运算符总是看起来很奇怪对我而言,部分因为这里发生的事情是A正在保存,而在下一个条款中,A的缺席意味着它实际上被删除< / em>的

list_set([A|X], Y):-
  list_set(X, Y),
  member(A, Y).

这只是前一条款的“其他”情况。它表示以A开头并且以X开头的列表对应于集合Y,前提是Y也对应于列表X而A已经在Y中。这就是从结果中删除元素的方式,因为A出现在第一个参数的模式,但不是在第二个参数的模式中。