现在学 Prolog 递归

转载自:https://mp.weixin.qq.com/s/r6CcuC8n7iRaQSzShovl5w

本章有两个主要目标:

1.在Prolog中引入递归定义。

2.表明Prolog程序的声明性含义与其程序含义之间可能存在不匹配。

 

1  递归定义

谓词可以递归定义。粗略地说,如果谓词中的一个或多个规则引用了自己,则它是递归定义的。

 

示例1Eating

考虑以下知识库:

 

    is_digesting(X,Y) :- just_ate(X,Y).

    is_digesting(X,Y) :-

        just_ate(X,Z),

        is_digesting(Z,Y).

 

    just_ate(mosquito,blood(john)).

    just_ate(frog,mosquito).

    just_ate(stork,frog).

 

乍一看,这似乎很普通:这只是一个包含三个事实和两个规则的知识库。但是is_digesting / 2谓词的定义是递归的。请注意,is_digesting / 2是(至少部分地)根据自身定义的,因为is_digesting / 2函子同时出现在第二条规则的头部和规则体中。但是,至关重要的是,这种循环性存在“逃离”的危险。这是由出现在第一条规则中的just_ate / 2谓词提供的。 (重要的是,第一条规则的规则体未提及is_digesting / 2。)现在,让我们考虑一下此定义的声明性和程序性含义。

“声明性”一词用于谈论Prolog知识库的逻辑含义。也就是说,Prolog知识库的声明性含义只是“它说了什么”,或者“如果我们将其理解为逻辑语句的集合,则意味着什么”。而且此递归定义的声明性含义非常简单。第一个子句(转义子句,即非递归子句,或者通常称为基子句)只是说:如果X刚吃掉Y,那么X现在正在消化Y。这显然是一个明智的定义。

那么第二个子句,即递归子句呢?这就是说:如果X刚吃掉Z并且Z在消化Y,那么X也在消化Y。同样,这显然是一个明智的定义。

因此,现在我们知道了此递归定义的含义,但是当我们提出实际上需要使用该定义的查询时会发生什么呢?也就是说,此定义实际上是做什么的?要使用正常的Prolog项,其程序含义是什么?

这也是相当简单的。基本规则类似于我们所见过的所有早期规则。也就是说,如果我们询问X是否正在消化Y,那么Prolog可以使用此规则来问一个问题:X刚刚吃了Y吗?

递归子句呢?这为Prolog提供了另一种确定X是否正在消化Y的策略:它可以尝试找到一些Z,使得X刚吃掉Z,而Z正在消化Y。也就是说,该规则使Prolog将任务分解为两个子任务。希望这样做最终会导致一些简单的问题,这些问题只需在知识库中查找答案即可解决。下图总结了这种情况:

让我们看看它是如何工作的。如果我们构成查询:

 

?-is_digesting(stork,mosquito).

 

然后Prolog如下工作。首先,它尝试利用列出的与is_digesting有关的第一条规则;即基本规则。这表明如果X刚吃了Y,X正在消化Y。通过将X与stork和Y与mosquito合一,它实现了以下目标:

 

no

 

但是知识库没有包含stork刚刚吃过mosquito的信息,因此这种尝试失败了。因此,Prolog接下来尝试利用第二条规则。通过将X与stork和Y与mosquito合一,可以实现以下目标:

 

just_ate(stork,Z),

is_digesting(Z,mosquito).

 

也就是说,为了显示is_digesting(stork,mosquito),Prolog需要找到Z的值,这样,首先,

 

just_ate(stork,Z).

 

其次,

 

is_digesting(Z,mosquito).

 

Z的值就是frog。立刻

 

just_ate(stork,frog).

 

将成功,因为此事实已列在知识库中。并推论

 

is_digesting(frog,mosquito).

 

几乎一样简单,因为is_digesting / 2的第一子句将此目标简化为

 

just_ate(frog,mosquito).

 

这是知识库中列出的事实。

好吧,这是我们第一个递归规则定义的例子。我们将学到更多有关它们的信息,但请立即提出一个非常实用的说明。希望很明显,当您编写一个递归谓词时,它应始终至少包含两个子句:一个基本子句(该子句在某个时候停止递归)和一个包含递归的子句。如果您不这样做,Prolog可能会陷入无休止的无用计算序列。例如,下面是一个非常简单的递归规则定义示例:

 

p:-p.

 

而已。没有其他的。简单而美丽。从声明性的角度来看,这是一个非常明智的定义(即使很无聊):它说“如果属性p成立,那么属性p成立”。你不能对此争论。

但是从程序角度来看,这是一个非常危险的规则。实际上,我们这里有最终的危险递归规则:双方完全一样,并且没有让我们脱离的基本子句。考虑一下当我们提出以下查询时会发生什么:

 

?- p.

 

Prolog问自己:“我怎么证明p?”的意思是:“嘿,我有一个规则!要证明p,我只需要证明p!”。因此,它再次问自己:“我怎么证明p?”并且意识到,“嘿,我有一个规则!要证明p,我只需要证明p!”。因此,它问自己(再次):“我怎么证明p?”并且意识到,“嘿,我有一个规则!要证明p,我只需要证明p!”,依此类推。

如果您进行此查询,那么Prolog不会回答您:它将继续前进,在无休止的搜索中拼命地运行。也就是说,它不会终止,您必须中断它。当然,如果使用跟踪,则可以一次完成一个步骤,直到厌倦了观看Prolog循环。

 

示例2:后代 

现在我们对Prolog中涉及的递归有所了解,现在该问为什么它如此重要了。实际上,这是一个可以在多个层面上回答的问题,但是现在,让我们保持实用性。因此:在编写有用的Prolog程序时,递归定义真的那么重要吗?如果是这样,为什么?

让我们考虑一个例子。假设我们有一个知识库,记录有关child关系的事实:

 

child(bridget,caroline).

child(caroline,donna).

 

也就是说,卡罗琳(Caroline)是布里奇特(Bridget)的孩子,唐娜(Donna)是卡罗琳(Caroline)的孩子。现在假设我们希望定义后代关系。也就是说,是孩子,或孩子的子女,或孩子的子女的子女,等等。这是第一次尝试这样做。我们可以在知识库中添加以下两个非递归规则:

 

descend(X,Y) :- child(X,Y).

 

descend(X,Y) :- child(X,Z),

     child(Z,Y).

 

现在,很明显,这些定义在一定程度上可行,但是它们显然受到限制:它们仅定义了两代或更少的后代。上面的知识库是可以的,但是假设我们获得了关于子关系的更多信息,并且我们将事实child列表扩展为:

 

child(anne,bridget).

child(bridget,caroline).

child(caroline,donna).

child(donna,emily).

 

  • 5
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值