Learn Prolog Now 翻译 - 第四章 - 列表 - 第二节,列表成员

内容提要

本章主要介绍使用递归操纵列表的一个实际例子:判断一个元素是否在包含在一个列表中。

是时候介绍第一个Prolog中通过递归操纵列表的程序例子了。我们最感兴趣的事情之一是,某个对象是否是列表中的元素。所以,我们想写一个程序,当假设输入是一个对象X和一个列表L,

得出结果是X是否属于L。这个程序的名字通常是:member,是Prolog程序中使用递归操纵列表最简单的例子,如下:

member(X, [X|T]).

member(X, [H|T]) :- member(X, T).

这就是全部的代码:一个事实(即member(X, [X|T]))和一个规则(即,member(X, [H|T]) :- member(X, T))。但是请注意这个规则是递归的(因为函子:member同时出现在规则的头部和

主干),它能够解释为什么这么短的程序就能够达到要求,让我们进一步分析。

首先我们从声明性方面解读这段程序。通过这种解读,会发现它是完全有意义的。第一个子句(即事实)简单地说:如果对象X是一个列表的头部,那么X就是列表的元素。请注意我们使用了

内置的“|”操作符去操作列表。

那么第二个递归的子句呢?它的含义是:如果对象X是一个列表尾部的元素,那么它同时也是这个列表的元素。同样注意使用“|”操作符操作列表。

现在,很明显这个定义具备很好的声明性含义。但是这个程序是否会像它声明性含义一样的去运行?即,程序是否真的能够求出一个对象X是否是一个列表L的元素?如果是,是如何做到的?

为了回答这些问题,我们需要思考程序性的含义。让我们通过以下简单的列子得出结果。

假设我们进行如下的查询:

?- member(yolanda, [yolanda, trudy, vincent, jules]).

Prolog会立即回答true。为什么?因为程序通过第一个子句(即事实),将yolanda和X合一,所以答案就立即给出。

接下来,思考下面的查询:

?- member(vincent, [yolanda, trudy, vincent, jules]).

现在第一个子句无法提供答案(vincent和yolanda是不同的原子),所以Prolog继续使用第二个递归规则的子句,它会给出新的目标:

member(vincent, [trudy, vincent, jules]).

现在第一个子句再一次无法提供答案,所以Prolog继续使用第二个递归规则的子句,它又给出新的目标:

member(vincent, [vincent, jules]).

这一次,第一个子句会提供答案,查询也就成功了。

到此为止一切都还好,但是我们需要问一个重要的问题,如果我们进行的查询失败了会发生什么?比如,如果进行查询:

member(zed, [yolanda, trudy, vincent, jules]).

现在,这个查询明显会失败(毕竟,zed不在列表中)。所以Prolog会如何处理?特别地,我们如何确认Prolog会终止,并且返回false,而不是陷入递归无限循环中?

让我们通过系统性思考来解答这个问题。又一次地,第一个子句无法提供答案,所以Prolog使用递归规则,会给出新的目标:

member(zed, [trudy, vincent, jules])

同样地,第一个子句无法提供答案,所以Prolog使用递归规则并且给出新的目标:

member(zed, [vincent, jules])

同样地,第一个子句无法提供答案,所以Prolog使用递归规则并且给出新的目标:

member(zed, [vincent])

现在第一个子句还是无法提供答案,所以Prolog使用递归规则并且给出新的目标:

member(zed, [])

现在到了有趣的时候,明显地第一个子句还是无法提供答案。但是请注意:递归规则也无法起作用了,为什么?很简单:递归规则依赖于将列表分解为头部和尾部,正如我们之前看到的,

空列表是无法再根据这种方式拆分的。所以递归规则不能再起作用了,所以Prolog停止了进一步搜索解决方案,并且报告false。即,Prolog告诉我们zed不属于这个列表,也正是我们期望

的答案。

我们可以对member/2这个谓词逻辑进行总结:它是一个递归的谓词逻辑,会系统地遍历整个列表去搜索答案。它通过将列表分解为更小的列表,并且搜索每一个更小列表的头部去进行匹

配。这种方法会导致搜索是递归的,而且因为这种递归是安全的(因为,不会陷入无限循环),最终Prolog必须涉及到空列表。空列表无法再分解为更小的列表,所以就可以结束掉递归。

好了,我们现在已经完全明白member/2的工作原理了,但是事实上,这个谓词逻辑远比之前的例子有用。之前我们只是通过它回答一些true/false的例子,但是我们能够在查询中使用变量,

比如,如果我们进行查询:

?- memeber(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).

这个版本从声明性和程序性上都是和第一个版本相同的。但是这个版本定义更加清晰:当你阅读的时候,你会集中在问题的本质上。

时间: 2024-12-26 01:20:50

Learn Prolog Now 翻译 - 第四章 - 列表 - 第二节,列表成员的相关文章

Learn Prolog Now 翻译 - 第三章 - 递归 - 第二节,规则顺序,目标顺序,终止

内容提要 规则顺序 目标顺序 终止 Prolog是第一门比较成功的逻辑编程语言.逻辑编程语言内在实现是简单和富有魅力的:程序员的工作简单地说就是描述问题:程序员应该写下(使用语言的逻辑)声明性的规格说明 (即,一个知识库),去描述有趣的状态.事实和关系:程序员不应该告诉计算机如何去实现,而他根据问一些问题去获取信息,逻辑编程语言会给出答案. 然而,以上是理想情况,Prolog本身也确实通过一些重要的特征,往这个方向在努力.但是Prolog不是,重复一次,不是一门完整的逻辑编程语言.如果你只是从声

Learn Prolog Now 翻译 - 第四章 - 列表 - 第一节,列表定义和使用

内容提要 列表定义: 合一在列表中的使用: 匿名变量: 列表定义 正如名字暗示的,列表就是多个元素组成的集合.更精确地说,是元素的有限序列.在Prolog中的列表,有如下的一些具体例子: [mia, vincent, jules, yolanda] [mia, robber(honey_bunny), X, 2, mia] [ ] [mia, [vincent, jules], [butch, girlfriend(butch)]] [[ ], dead(z), [2, [b, c]], [ ]

Learn Prolog Now 翻译 - 第四章 - 列表 - 第三节,递归遍历列表

内容提要 通过递归对列表进行遍历,从而完成各种操作. member/2这个谓词逻辑通过递归遍历了列表,对列表头部有一些操作,然后递归地对列表尾部做另外一些相同的操作.通过递归遍历列表在Prolog是十分普遍的做法, 事实上,我们必须要掌握这项技能.所以我们学习如下的例子. 当我们使用列表的时候,我们经常会将一个列表和另一个列表进行对比,或者拷贝一个列表的内容到另一个列表去,或者翻译一个列表到内容到另一个列表去,或者 类似到一些操作.这里有一个例子,假设我们有一个谓词a2b/2,有两个参数,第一个

Learn Prolog Now 翻译 - 第四章 - 列表 - 第四节,练习题和答案

练习题4.1 Prolog将会如何回答下面的查询? 1. [a, b, c, d] = [a, [b, c, d]]. 2. [a, b, c, d] = [a | [b, c, d]]. 3. [a, b, c, d] = [a, b, [c, d]]. 4. [a, b, c, d] = [a, b | [c, d]]. 5. [a, b, c, d] = [a, b, c, [d]]. 6. [a, b, c, d] = [a, b, c | [d]]. 7. [a, b, c, d] =

Learn Prolog Now 翻译 - 第三章 - 递归 - 第一节,递归的定义

在Prolog中,谓词可以递归地定义.简要地讲,一个谓词是递归定义的,如果一个或者多个规则的定义中包含了规则自身. 例子1:消化 考虑如下的知识库: 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

Learn Prolog Now 翻译 - 第六章 - 列表补遗 - 第一节,列表合并

内容提要: 列表合并的定义 列表合并的使用 列表合并的定义 我们将会定义一个很重要的谓词:append/3,其中所有的参数都是列表.从声明性角度去看,append(L1, L2, L3)的含义是列表L3是列表L1和列表L2的合并结果(合并意味着连接).比如, 如果我们查询: ?- append([a, b, c], [1, 2, 3], [a, b, c, 1, 2, 3]). 或者查询: ?- append([a, [foo, gibble], c], [1, 2, [[], b]], [a,

Learn Prolog Now 翻译 - 第五章 - 数字运算 - 第一节,Prolog中的数字运算

内容提要 Prolog中的数字运算 Porlog运算的本质 Prolog中的数字运算 Prolog语言本身提供了一些基础的运算符号,对整数进行一些操作(即类似...-3, -2, -1, 0, 1, 2, 3等).多数Prolog的实现同时也提供了一些工具对实数进行操作 (比如浮点数,1.53,6.35,等等).但是我们不会讨论浮点数,因为浮点数在典型的Prolog应用中很少,所以不是本文的重点.但是另一方面,整数是在Prolog 中有典型应用场景的(比如记录列表的长度),所以掌握起应用是十分重

Learn Prolog Now 翻译 - 第三章 - 递归 - 第四节,更多的实践和练习

在学习了前三章内容后,我们应该对Prolog编程有了直观和理性的认识.由于合一.变量初始化.证明搜索和递归都是Prolog的核心概念,所以有如下更多的一些实践和练习. 这里我会先录入题目,后期再给出我自己的程序代码和一些思考. 实践1 试想有如下的描述迷宫的知识库.其中的事实描述了点和点之间的联通关系,即connected/2谓词逻辑给出了这样的事实:迷宫中能从参数1的点,直接到达参数2的点.而且, 联通关系是有方向的.单向不能往返的: connected(1,2). connected(3,4

Learn Prolog Now 翻译 - 第五章 - 数字运算 - 第四节,练习题和答案

练习题5.1  Prolog会如何回答下面的问题? 1. X = 3*4. 2. X is 3*4. 3. 4 is X. 4. X = Y. 5. 3 is 1+2. 6. 3 is +(1,2). 7. 3 is X+2. 8. X is 1+2. 9. 1+2 is 1+2. 10. is(X, +(1,2)). 11. 3+2 = +(3,2). 12. *(7,5) = 7*5. 13. *(7, +(3,2)) = 7*(3+2). 14. *(7, (3+2)) = 7*(3+2)