Learn Prolog Now!第二章,第一节 合一

2.1 合一

在讲KB4的时候,我们曾简单提及合一(prolog把woman(X) 与 woman(mia)合一,X实例化为mia)。现在是时候仔细看看合一了,这是prolog的最基本思想之一。

回顾一下三种元素:

  1. 常量:原子(vincent)、数字。
  2. 变量:(X,Z3,List)
  3. 复杂元素:如 functor(term_1,...,term_n) .

我们从KB4的例子入手讲解prolog合一两个元素的本质。虽然你已经有了印象,但还有一些细节要注意:

如果两个元素要合一,必要满足下面下面两个条件之一:

  • 这是两个相同的元素
  • 两个元素内的变量实例化后,两个元素相同

举一些例子来帮助理解:

  • mia和mia合一,它们是相同原子
  • 42和42合一,相同数字
  • X和X合一,相同变量
  • woman(mia) 和 woman(mia) 合一,相同的复杂元素
  • woman(mia) 和 woman(vincent) ,无法合一。不仅不相同,而且没有包含可以实例化为相同元素的变量
  • mia和X,合一。虽然它们不同,但是X可以被实例化为mia,
  • loves(vincent,X) 和 loves(X,mia) ,无法合一。无法找到一个X的实例,使两元素相等。(loves(vincent,vincent) and loves(vincent,mia),loves(vincent,mia) and loves(mia,mia) )

那么,变量是如何被实例化相等的呢?prolog告诉我们。当prolog合一两个元素时,会尝试变量所有可能的实例化。这让我们能构建复杂元素(递归结构的元素),使合一成为了一个强大的编程机制。

有了第一印象,再看看合一的精确定义。它不仅告诉我们prolog会合一哪些元素,也会告诉我们prolog是如何操作变量的。

  1. 如果 term1 和 term2是常量,只有在它们都是相同数字或都是相同原子时,才合一。
  2. 如果 term1 是变量,term2是任意元素,则它们合一,term1实例化为term2。反之亦然。若term1 term2均为变量,它们均实例化为对方,我们称它们为共享值
  3. 如果 term1 和 term2都是复杂元素,则它们只有在以下情况都符合时会合一
    1. 它们要有相同的函数名和参数数量
    2. 对应的参数要合一
    3. 变量的实例化要匹配(loves(vincent,X) 和 loves(X,mia)就是不匹配的情况)
  4. 当且仅当它们遵循上面三个定义之一时,两个元素可以合一。

注意第三条定义的结构,它的三条子句完美的展现了复杂元素的(递归)结构。

例子

| ?- =(mia,mia).

yes
| ?- =(mia,vicent).

no

结果显而易见,但我们通常不会这么查询。把等号写在前面很不自然,我们通常用中缀方式来表示(就是把等号放在两个参数中间):

| ?-  mia  =  mia.

yes

它们两个可以合一,显然是因为其符合定义的第一条,它们两个都是原子。

| ?- 2  =  2.

yes
| ?- mia  =  vincent. 

no

上面的结果也很显然。

| ?- 'mia'=mia.

yes

让我们看看上面发生了什么。prolog认为,'mia'和mia是同一个原子。prolog会把'something'看作和something一样的原子,这个特性在某些类型的程序中很有用,要记好了。

| ?-'2'=2.

no

这又是为什么呢?

prolog认为'2'是原子,而2是数字,不符合第一条定义。

| ?- mia  =  X. 

X = mia

yes

这个也很好懂。

| ?- X  =  Y. 

yes

如第三条所说,XY表示了同一个对象。有的版本prolog会输出下面的信息:

| ?- X  =  Y. 

X  =  _5071 
Y  =  _5071 
yes

首先要知道_5071是匿名变量,当两个变量合一,它们共享变量值。prolog为此开辟了一个新的匿名变量( _5071),XY都分享了这个变量的值,5071没有什么特殊的意义,只是prolog创建的一个普通的随机变量名,就和 _5075和 _6189一样。

| ?-(X,Y,X)=(Y,1,2).

no

译者注:我自己对例子做了一些补充。XY共享值,所以他们的值应该相同,所以返回no。

?-  X  =  mia,  X  =  vincent.

no

如果把这两个目标(goals)拆开运行,prolog都会返回yes。但是prolog先运行了第一个目标,X被实例化为mia,所以X无法与vincent合一。一个被实例化的变量已经不能算是变量了,它变成了它所实例化的对象。

让我们看看涉及复杂元素的例子吧:

| ?- k(s(g),Y)  =  k(X,t(k)).

X = s(g)
Y = t(k)

yes

显然,它符合第三条定义,并且根据第二条定义, s(g) 和 X也可以合一, 所以两个复杂元素可以合一。

| ?- k(s(g),  t(k))  =  k(X,t(Y)). 

X = s(g)
Y = k

yes
| ?- loves(X,X)  =  loves(marcellus,mia).

no

以此类推,上面的答案也很容易得出。

触发检查

合一是一个广为人知的概念,计算机科学的很多分支都有用到。它已经被彻底研究过,许多合一的算法都是众所周知的了。但prolog并未采用标准的合一算法。它选择了一条捷径,你要学会它。

考虑一下下面的查询:

?-  father(X)  =  X.

能合一吗?标准合一算法会说:“不”。

为什么呢?任意选一个元素与X合一。比如你把X实例化为 father(father(butch)) ,左边就会变成 father(father(father(butch))),右边就会变成father(father(butch))。显然无法合一。无论X实例化为什么,左边都会比右边多一层father()。标准的算法会发现这点,暂停,告诉我们no。

上面给出的prolog递归合一不会这么做。因为它的右边是X,根据第二条定义认定它们可以合一。随后,把X实例化成左边,father(X)。然而这里面有一个X,而X被实例化为father(X)了,所以prolog意识到,father(X)事实上是father(father(X)),等等等等。prolog会不断扩充一个没有结束的序列。

上面都是理论,事实上会发生什么呢?在一些老的prolog实现上,我们所说的确实会发生,你会看到:

Not  enough  memory  to  complete  query!

还有一长串字符:

X  =  father(father(father(father(father(father 
         (father(father(father(father(father(father 
         (father(father(father(father(father(father 
         (father(father(father(father(father(father 
         (father(father(father(father(father(father

prolog在拼命的想返回一个正确的实例化元素,然而实例化的过程时无边无际的,它停不下来。从抽象的数学角度来看,prolog的做法是明智的。直觉上,只有当X实例化为一个用father构造的无限长的元素时,左边多出来的那个father造成的影响才可以被忽略。但我们只能处理有限项目,无限只是一种有趣的抽象的数学。无论prolog怎么努力,也无法做到无限。

因此让prolog耗尽内存会让人很苦恼。更先进的prolog(比如SWI Prolog 或者 SICStus Prolog)实现找到了一种更优雅的方法来处理。它们也认为这个合一是存在的,但它们不会真的天真的去不断实例化。他们会发现有一个潜在的问题,然后停下来,宣布合一存在,输出以下有限字符来表示这个无限的元素:

father(father(father(father(...))))))))

那么上面这种无限的元素可不可以用来计算呢?结果取决于你用的prolog实现,有的可以,有的不行。如下例子:

X  =  father(X),  Y  =  father(Y),  X  =  Y.

有的会导致崩溃(X=Y是要合一两个有限表示的无限元素),尽管如此,在一些现代系统(比如SWI 和 Sicstus)中这样的合一也会很有用,你可以在你的程序中运用它们。然而,它可能的用处和它的实质不在本书的范围内。

简而言之,对于“father(X)是否与X合一”这个问题有三种回答

  • 标准合一算法:no
  • 老的prolog实现:一直运行,直到内存用尽。
  • 更先进的prolog实现:yes,然后返回一个用有限表示的无限元素

没有什么是真正正确的答案,你需要理解个中差别,了解你用的prolog实现是如何处理这个问题的。

在这个部分的最后,希望你能在prolog上自己操作一下上面的例子。这里,我们想再进一步说明prolog的合一和标准的合一之间的区别。在上面例子中,我们可以看出,当它们面对X和father(X)合一时,是有很大不同的。

  • 标准算法:当合一两个元素前,会先执行触发检查。也就是说,当合一一个变量和一个元素时,它们会先检查元素里是否存在这个变量。如果存在,算法就认定合一不可能。只有当不存在时,算法才会尝试继续合一。

    换句话说,标准合一算法是悲观的。它们会先检查,确定是安全的以后才继续尝试合一。所以,标准合一算法永远不会陷入无尽的尝试实例化变量,不会出现无穷元素。

  • prolog算法:乐观的算法。它相信你不会给它危险的东西。所以它走了一个捷径:它跳过了触发检查,直接进行合一。作为一个编程语言,这是一个很明智的策略。合一是prolog最基础的操作,所以它需要运行的越快越好。每次都进行检查就会拖慢速度。所以,悲观是安全的,但是,乐观能更快!毕竟,在实际程序中,你几乎不会做出X = father(X)这样的操作。

最后一提,prolog有一个执行标准合一(带有触发检查)的内建谓词:

unify_with_occurs_check/2.

所以,当我们查询:

?-  unify_with_occurs_check(father(X),X).

no

用合一来编程

合一是prolog的基础操作,在Prolog的证明搜索(proof search)中起着很关键的作用(我们待会会学)。随着你深入了解,合一会别的越来越有趣和重要。如果你要写一个有用的程序,用复杂元素可以定义那些有趣的概念,合一才能找到你想要的信息。

下面就是一个例子,两条事实分别定义的什么是垂直(vertical)线和水平(horizontal)线:

vertical(line(point(X,Y),point(X,Z))). 
    
horizontal(line(point(X,Y),point(Z,Y))).

这个知识库看起来很简单有趣,我们仔细看看。首先,这个复杂元素有三层。最内层的是point函子和两个参数,point(X,Y)就代表了笛卡尔坐标系上的一个点。我们有了两个不同的点,就可以以此定义一条线。所以,两个point元素就被作为line函子的两个参数。XY分别是点到原点的垂直和水平距离。

垂直和水平是线的两种属性。所以vertical和horizontal要一个line作参数。vertical/1的定义:由相同X坐标的两点组成的线是垂直的。两个point的第一个参数都是X,以此来表示“相同的X坐标”。

水平也是同理,两个point的第二个参数都是Y,以此来表示“相同的Y坐标”。

我们可以怎么用知识库?看下面这个例子:

?-  vertical(line(point(1,1),point(1,3))). 

yes
?-  vertical(line(point(1,1),point(3,2))). 

no
?- horizontal(line(point(1,1),point(2,Y))). 

Y = 1

yes

看下面这个例子:

?- horizontal(line(point(2,3),P)). 

P = point(_1972,3)

yes

这个查询的意思是,哪一个点P和点(2,3)组成的线是平行线。答案是,任何y坐标是3的线。_1972是一个变量,prolog的意思是任意x坐标都行。

合一结构在prolog中很强大,远比这个例子强大。此外,用大量合一编写的程序也会是高效的。在第七章讨论不同列表时,我们会学到一个漂亮的例子。

这样的编程风格很有用,面对一些自然的层次结构(比如上面的例子),我们就可以用复杂元素来表示这个结构,通过合一来使用它。这种方法在计算机语言学上就非常重要,因为语言本身是有自然层次结构的(想一想,一个句子可以由名词词组和动词词组组成,名词词组也可以由定语和名词组成,等等等等)。

你可能感兴趣的:(Learn Prolog Now!第二章,第一节 合一)