我正试图通过阅读Ulle Endriss的讲义来了解Prolog编程。当我对一个练习的解决方案不如预期时,我发现很难给出一个好的解释。我认为这与我对Prolog评估表达式的方式的不稳定理解有关。
第20页的练习2.6要求递归实现谓词last1
,其行为类似于内置谓词last
。我的尝试如下:
last1([_ | Rest], Last) :- last1(Rest, Last). last1([Last], Last).
它给出了正确的答案,但对于包含多个元素的列表,我必须键入分号来终止查询。这使得last1
不同于内置的last
。
?- last1([1], Last).
Last = 1.
?- last1([1, 2], Last).
Last = 2 ;
false.
如果我切换声明规则和事实的顺序,那么在这两种情况下都需要键入分号。
我想我知道为什么Prolog认为last1
可能还有一个解决方案(因此是分号)。我想它遵循的评估顺序
last1([1, 2], Last).
==> last1([2], Last).
==> last1([], Last). OR Last = 2.
==> false OR Last = 2.
这似乎表明我应该寻找一种方法来避免将Rest
与[]
匹配。无论如何,我无法解释为什么改变申报顺序会产生任何影响。
问题1:对last1
行为的正确解释是什么?
问题2:如何实现与内置last
无法区分的谓词last1
?
问题1:
Prolog系统并不总是能够在执行子句之前决定是否适用。具体情况取决于实现。也就是说,总的来说,你不能依赖这个决定。从一个版本到另一个版本,系统确实有所改进。考虑最简单的情况:
?- X=1;1=2X=1;false。
一个非常聪明的Prolog可以检测到1 = 2
总是失败,因此只需回答X = 1.
。另一方面;聪明;实现起来成本非常高,并且时间可以更好地用于优化更频繁的情况。
那么为什么Prolog会显示这一点呢?如果Prolog已经知道没有进一步的答案,那么主要的原因是避免温顺地要求另一个答案。因此,在这一改进之前,系统会提示您为所有包含变量的查询提供另一个答案,并得到false
或"0";否";每个问题都有一个答案。这曾经是如此麻烦,以至于许多程序员从未询问下一个答案,因此也没有被提醒注意意外的答案。
第二个原因是让你意识到实现的局限性:如果Prolog要求对这个通用查询给出另一个答案,这意味着它仍然使用一些空间,这些空间可能会积累并消耗掉你所有的计算资源。
在您的last1/2
示例中,您会遇到这样的情况。顺便说一句,你已经做了一些非常聪明的事情:你试图最小化查询,以查看意外行为的第一次出现。
在您的示例查询last1([1,2],X)
中,Prolog系统不查看整个列表[1,2]
,而只查看主函子。因此,对于Prolog系统,当它决定应用哪些子句时,查询看起来与last1([_|_],X)
相同。这个目标现在适用于这两个子句,这就是Prolog将记住第二个子句作为尝试的替代方案的原因。
但是,想想看:这个选择现在对所有元素都是可行的,但最后一个元素除外!这意味着你要为每个元素付出一些内存!实际上,你可以通过使用一个很长的列表来观察这一点。这是我在32位笔记本电脑上得到的——你可能需要在一个更大的系统上再加一两个零:
?- 长度(L,10000000),last1(L,E)resource_error(_).%错误:本地堆栈不足
另一方面,预定义的last/2
工作平稳:
?- 长度(L,10000000),最后(L,E)L=[A,_B,_C,_D,_E,_F,_G,_H,_I|…]。
事实上,它使用常量空间!
现在有两种方法可以解决这个问题:
-
试着优化你的定义。是的,你可以这样做,但你需要非常聪明!例如,@back_dragon的定义是不正确的。初学者经常试图优化程序,而实际上他们正在破坏程序的语义。
-
问问自己,你是否真的在定义与
last/2
相同的谓词。事实上,你不是。
问题2:
考虑:
?- 最后(Xs,X)Xs=[X];Xs=[A,X];Xs=[A,_B,X];Xs=[A,_B,_C,X];Xs=[A,_B,_C,_D,X]。
和
?- last1(X,X)循环。
因此,在这种情况下,您的定义与SWI的定义不同。交换条款的顺序。
?- 长度(L,10000000),last2(L,E)L=[A,_B,_C,_D,_E,_F,_G,_H,_I|…];false。
再次,这个false
!但这一次,大名单奏效了。这一次,最小的查询是:
?- last2([1],E)E=1;false。
情况非常相似:同样,Prolog将以与last2([_|_],E)
相同的方式查看查询,并得出两个子句都适用的结论。至少,我们现在有恒定的开销,而不是线性开销。
有几种方法可以以干净的方式克服这种开销,但它们都在很大程度上取决于实现的内部结构。
SWI Prolog在确定没有解决方案时,试图避免提示提供更多解决方案。我认为解释器检查内存,寻找一些剩余的choice point
,如果找不到,只需声明终止。否则,它将等待用户选择移动。
我会尝试以这种方式使last1具有确定性:
last1([_,H|Rest], Last) :- !, last1([H|Rest], Last).
last1([Last], Last).
但我不认为是indistinguishable
。潜伏在库的源代码(简单如?- edit(last).
)
%% last(?List, ?Last)
%
% Succeeds when Last is the last element of List. This
% predicate is =semidet= if List is a list and =multi= if List is
% a partial list.
%
% @compat There is no de-facto standard for the argument order of
% last/2. Be careful when porting code or use
% append(_, [Last], List) as a portable alternative.
last([X|Xs], Last) :-
last_(Xs, X, Last).
last_([], Last, Last).
last_([X|Xs], _, Last) :-
last_(Xs, X, Last).
我们可以欣赏一个经过深思熟虑的实现。
此代码可以工作:
last1([Last], Last).
last1([_ | Rest], Last) :- last1(Rest, Last), !.
这是因为prolog的东西可能有更多的组合,但是,有了这个符号:!,prolog在到达这一点之后不会返回