转载自:https://mp.weixin.qq.com/s/CssLyqHaXRdUFW1UUiMwNw
2 成员
现在该来看一下我们用于处理表的递归Prolog程序的第一个示例。我们想知道的最基本的事情之一就是某物是否是表的元素。因此,让我们写一个程序,当输入任意对象X和表L作为输入时,它告诉我们X是否属于L。执行此操作的程序通常称为成员,这是Prolog程序的最简单示例利用表的递归结构。这里是:
member(X, [X|T]).
member(X, [H|T]):- member(X,T).
这就是全部:一个事实(即member(X, [X|T]))和一个规则(即member(X, [H|T]):-member(X, T))。但是请注意,该规则是递归的(毕竟,函子成员出现在规则的头部和规则体中),这就是为什么需要这么短的程序的原因。让我们仔细看看。
我们将从声明式阅读程序开始。并以此方式阅读,这显然是明智的。第一个子句(事实)简单地说:如果对象X是该表的头部,则它是该表的成员。请注意,我们使用了内置|操作符陈述关于表的这一(简单但重要的)原理。
第二个子句,递归规则呢?这表示:如果对象X是表尾部的成员,则它是表的成员。再次注意,我们使用了|操作符这个原理的声明。
现在,显然此定义具有良好的声明意义。但是,该程序实际上执行了应做的事情吗?也就是说,它真的可以告诉我们对象X是否属于表L吗?如果是这样,它到底是如何做到的?要回答此类问题,我们需要考虑其程序含义。让我们通过一些示例进行工作。
假设我们提出了以下查询:
?- member(yolanda, [yolanda,trudy,vincent,jules]).
Prolog将立即回答true。为什么?因为它可以将member / 2的第一个子句(事实)中X的两次出现合一为yolanda,所以它立即成功。
接下来考虑以下查询:
?- member(vincent, [yolanda,trudy,vincent,jules]).
现在,第一个规则无济于事(vincent和yolanda是不同的原子),因此Prolog转到第二个子句,即递归规则。这给Prolog一个新的目标:现在必须查看是否
member(vincent, [trudy,vincent,jules]).
同样,第一个子句无济于事,因此Prolog(再次)采用了递归规则。这给了它一个新的目标,即
member(vincent, [vincent,jules]).
这次,第一个子句确实有帮助,查询成功。
到目前为止一切顺利,但我们需要提出一个重要问题。当我们提出失败的查询时会发生什么?例如,如果我们提出如下查询会发生什么
member(zed, [yolanda,trudy,vincent,jules]).
现在,这显然应该失败了(毕竟zed不在表中)。那么Prolog如何处理呢?特别是,我们如何确定Prolog确实会停止,然后说不,而是进入一个无限递归循环中?
让我们系统地思考一下。再一次,第一个子句无济于事,因此Prolog使用了递归规则,这给了它一个新的目标
member(zed, [trudy,vincent,jules]).
同样,第一个子句无济于事,因此Prolog重用了递归规则并试图证明
member(zed, [vincent,jules]).
同样,第一条规则也无济于事,因此Prolog再次重用第二条规则并尝试实现目标
member(zed, [jules]).
同样,第一个子句无济于事,因此Prolog使用第二个规则,这给了它目标
member(zed, []).
这就是事情变得有趣的地方。显然,第一 个子句在这里无济于事。但请注意:递归规则也无能为力。为什么不?很简单:递归规则依赖于将表分为头和尾,但正如我们已经看到的那样,无法以这种方式拆分空表。因此,递归规则也无法应用,并且Prolog停止搜索更多解决方案,并宣布false。也就是说,它告诉我们zed不属于该表,而这正是它应该做的。
我们可以将member / 2谓词总结如下。这是一个递归谓词,可以系统地在表的长度中搜索所需项。它通过将表逐步细分为较小的表,然后查看每个较小表的第一项来完成此操作。驱动此搜索的这种机制是递归,并且此递归安全的原因(即,它不会永远持续下去的原因)是,在行尾,Prolog必须询问有关空表的问题。空表不能细分为较小的部分,这允许您退出递归。
好了,我们现在已经知道了member / 2为何起作用,但实际上它比前面的示例所暗示的要有用得多。到目前为止,我们仅使用它来回答true/false问题。但是我们也可以提出包含变量的问题。例如,我们可以使用Prolog创建以下对话框:
member(X, [yolanda,trudy,vincent,jules]).
X = yolanda ;
X = trudy ;
X = vincent ;
X = jules ;
false.
也就是说,Prolog告诉我们表的每个成员是什么。这是member / 2的一种非常普通的用法。实际上,通过使用变量,我们对Prolog说:“快!请给我一些表元素!”。在许多应用程序中,我们需要能够提取表的成员,而这通常是这样做的方式。
最后一句话。我们上面定义的member / 2的方法当然是正确的,但从某种角度来说,这有点混乱。
想一想。第一个子句在那里处理表的开头。但是,尽管尾部与第一个子句无关,但我们使用变量T来命名尾部。类似地,递归规则也可以用来处理表的尾部。但是尽管这里的头部无关紧要,但我们使用变量H对其进行了命名。这些不必要的变量名称令人分心:最好以专注于每个子句中真正重要内容的方式编写谓词,而匿名变量为我们提供了这样做的好方法。也就是说,我们可以如下重写member / 2:
member(X,[X|_]).
member(X,[_|T]):- member(X,T).
无论是声明性的还是程序性的,此版本都是完全相同的。但是,这一点更加清晰:阅读时,您不得不专注于基本内容。