14

我试图通过阅读 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: 我怎样才能实现一个last1与内置的无法区分的谓词last

4

3 回答 3

13

问题一:

Prolog 系统并不总是能够在执行之前决定一个子句是否适用。具体情况取决于实现。也就是说,您一般不能依赖该决定。系统确实从一个版本到另一个版本有所改进。考虑作为最简单的情况:

?- X = 1 ; 1 = 2。
X = 1;
错误的。

一个非常聪明的 Prolog 可以检测到它1 = 2总是失败,因此只是简单地回答X = 1.。另一方面,这种“聪明”的实现成本非常高,最好将时间花在优化更频繁的情况上。

那么,为什么 Prologs 会显示这一点呢?主要原因是避免温顺地询问另一个答案,如果 Prolog 已经知道没有进一步的答案。因此,在此改进之前,系统会提示您为所有包含变量的查询提供另一个答案,并false在每个查询中得到“否”或“否”,并且只有一个答案。这曾经非常麻烦,以至于许多程序员从不要求下一个答案,因此不会收到有关意外答案的警报。

第二个原因是让您了解实现的局限性:如果 Prolog 要求对这个通用查询提供另一个答案,这意味着它仍然使用一些空间,这些空间可能会积累并耗尽您的所有计算资源。

在你的例子中last1/2你遇到了这样的情况。而且您已经做了一些非常聪明的事情,顺便说一句:您尝试最小化查询以查看意外行为的第一次出现。

在您的示例查询last1([1,2],X)中,Prolog 系统不会查看整个列表[1,2],而只会查看主函子。last1([_|_],X)因此,对于 Prolog 系统,查询看起来与决定应用哪些子句时相同。这个目标现在适用于两个子句,这就是 Prolog 将记住第二个子句作为尝试替代的原因。

但是,想一想:除了最后一个元素之外,现在所有元素都可以选择这个选项!这意味着您为每个元素支付一些内存!您实际上可以通过使用很长的列表来观察这一点。这是我在我的小型 32 位笔记本电脑上得到的——您可能需要在更大的系统上再添加零或两个:

?-长度(L,10000000),last1(L,E)。
错误:超出本地堆栈

另一方面,预定义的last/2工作顺利:

?-长度(L,10000000),最后(L,E)。
L = [_G343,_G346,_G349,_G352,_G355,_G358,_G361,_G364,_G367|...]。

事实上,它使用的是常数空间!

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

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

  2. 问问自己,你是否真的定义了与 相同的谓词last/2。事实上,你不是。

问题2:

考虑:

?-最后(Xs,X)。
Xs = [X] ;
Xs = [_G299, X] ;
Xs = [_G299, _G302, X] ;
Xs = [_G299,_G302,_G305,X];
Xs = [_G299, _G302, _G305, _G308, X]
...

?- last1(Xs, X)。
** 循环 **

因此,在这种情况下,您的定义与 SWI 的定义不同。交换条款的顺序。

?-长度(L,10000000),last2(L,E)。
L = [_G343,_G346,_G349,_G352,_G355,_G358,_G361,_G364,_G367|...];
错误的。

再次,这个false!但这一次,大名单奏效了。而这一次,最小的查询是:

?- last2([1],E)。
E = 1 ;
错误的。

情况也很相似:同样,Prolog 将以相同的方式查看查询,last2([_|_],E)并得出两个子句都适用的结论。至少,我们现在有恒定的开销而不是线性开销。

有几种方法可以以干净的方式克服这种开销 - 但它们都非常依赖于实现的内部结构。

于 2012-07-31T08:29:04.170 回答
5

当 SWI-Prolog 可以确定没有解决方案时,它会尝试避免提示更多解决方案。我认为解释器检查内存以寻找choice point剩余的内存,如果找不到,只需声明终止。否则它会等待让用户选择移动。

我会尝试以这种方式使 last1 具有确定性:

last1([_,H|Rest], Last) :- !, last1([H|Rest], Last).
last1([Last], Last).

但我不认为它是indistinguishablelast开始的。潜伏在库的源代码处(很简单?- 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).

我们可以欣赏一个深思熟虑的实施。

于 2012-07-31T10:34:39.243 回答
0

这段代码可以工作:

last1([Last], Last).
last1([_ | Rest], Last) :- last1(Rest, Last), !.

这是因为 prolog 的东西可能有更多的组合,但是,使用这个符号:!,prolog 到达这一点后不会返回

于 2015-10-11T14:54:36.457 回答