在Prolog中实现"last"



我正试图通过阅读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|…]。

事实上,它使用常量空间!

现在有两种方法可以解决这个问题:

  1. 试着优化你的定义。是的,你可以这样做,但你需要非常聪明!例如,@back_dragon的定义是不正确的。初学者经常试图优化程序,而实际上他们正在破坏程序的语义。

  2. 问问自己,你是否真的在定义与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在到达这一点之后不会返回

最新更新