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, [foo, gibble], c, 1, 2, [[], b]]).

Prolog会回答true。

但是,如果我们查询:

?- append([a, b, c], [1, 2, 3], [a, b, c, 1, 2]).

或者查询:

?- append([a, b, c], [1, 2, 3], [1, 2, 3, a, b, c]).

Prolog会回答false。

从程序性的角度来说,append/3最为有用的作用是连接两个列表。我们可以在第三个参数中使用变量,轻松地达到目的:

?- append([a, b, c], [1, 2, 3], L3).

L3 = [a, b, c, 1, 2, 3].

但是(我们将会看到)我们也可以使用append/3分割列表。事实上,append/3是一个真正多用途的谓词,我们可以通过它做很多事情,并且通过学习这个谓词,我们可以更好地理解在

Prolog中如何操作列表。

如下是append/3的定义:

append([], L, L).

append([H|T1], L2, [H|T3]) :- append(T1, L2, T3).

这是一个递归形式的定义。基础子句很简单:将一个空列表和任意列表进行合并,那么结果是和任何列表相同的列表,这显然是正确的。

那么递归的部分呢?含义是:如果我们将一个非空列表,[H|T]和另外一个列表L2进行合并,那么得到的列表是头部为H,并且尾部是T和L2合并的结果。图示可能会更清楚:

Input: [H | T ] + L2

Result: [ H |  T + L2]

但是这个定义的程序性含义是什么?当两个列表发生合并时实际会如何操作?让我们根据细节的分解来进一步学习,例子还是查询:

?- append([a, b, c], [1, 2, 3], X).

当我们进行这个查询时,Prolog将会使用递归子句去进行匹配,并且生成一个新的中间变量(比如称为_G518),如果我们跟踪接下来的步骤,可能如下:

append([a, b, c], [1, 2, 3], _G518).

append([b, c], [1, 2, 3], _G587).

append([c], [1, 2, 3], _G590).

append([], [1, 2, 3], _G593).

append([], [1, 2, 3], [1, 2, 3]).

append([c], [1, 2, 3], [c, 1, 2 ,3]).

append([b, c], [1, 2, 3], [b, c, 1, 2, 3]).

append([a, ,b, c], [1, 2, 3], [a, b, ,c, 1, 2, 3]).

X = [a, b, c, 1, 2, 3].

true

这种求解的模式很清晰:前四行可以看见Prolog通过递归定义的方法遍历完第一个列表;然后,接下来的四行显示,Prolog接下来回填每个中间结果,如何执行的呢?通过依次初始化变量:

_G593,_G590,_G587和_G518。但是这里学习的关键是把握这种基础模式,而不仅仅局限于append/3的实现。所以,我们可以更深入地研究,如下是查询,append([a, b, c], [1, 2, 3], X)

的搜索树,我们将会仔细标记每一个步骤,每一个中间目标,及其每一个变量的初始化值:

1. Goal 1:append([a, b, c], [1, 2, 3], _G518)。Prolog将其和递归规则的头部合一。所以_G518和[a | T3]合一,同时Prolog有了新的目标,append([b, c], [1, 2, 3], T3),这会为T3生成

一个新的变量,_G587,所以我们可以知道:_G518 = [a | _G587]。

2. Goal 2:append([b, c], [1, 2, 3], _G587)。Prolog将其和递归规则的头部合一,所以_G587和[b | T3]合一,Prolog有了新的目标,append([c], [1,2 ,3], T3),这会为T3生成一个新的变量,

_G590,所以我们可以知道:_G587 = [b | _G590]。

3. Goal 3:append([c], [1, 2, 3], _G590)。Prolog将其和递归规则的头部合一,所以_G590和[c | T3]合一,Prolog有了新的目标,append([], [1, 2, 3], T3),这会为T3生成一个新的变量,

_G593,所以我们可以知道:_G590 = [c | _G593]。

4. Goal 4:append([], [1, 2, 3], _G593)。最终,Prolog可以使用基础子句(即,append([], L, L))了,所以在接下来的连续4个步骤,Prolog会获取Goal 4, Goal 3, Goal 2, Goal 1的答案:

5. Goal 4 的答案: append([], [1, 2, 3], [1, 2, 3]),这是因为当我们使用基础子句匹配Goal 4时,_G593将和[1, 2, 3]合一。

6. Goal 3 的答案: append([c], [1, 2, 3], [c, 1, 2, 3]),为什么?因为 Goal 3是,append([c], [1, 2, 3], _G590),同时_G590是列表[c | _G593],我们已经将_G593和[1, 2, 3]合一,所以

_G590将和[c, 1, 2, 3]合一。

7. Goal 2 的答案:append([b, c], [1, 2, 3], [b, c, 1, 2, 3])。为什么?因为Goal 2是,append([b, c], [1, 2, 3], _G587),同时_G587是列表[b | _G590],我们已经将_G590和[c, 1, 2, 3]合一,

所以_G587将和[b, c, 1, 2, 3]合一。

8. Goal 1 的答案:append([a, b, c], [1, 2, 3], [a, b, c, 1, 2, 3]),为什么?因为Goal 1是,append([a, b, c], [1, 2, 3], _G518),同时_G518是列表[a | _G587],我们已经将_G587和

[b, c, 1, ,2 ,3]合一,所以_G518将和[a, b, c, 1, 2, 3]合一。

9. 所以Prolog现在已经知道如何初始化X,这个原始的查询变量,它会告诉结果,X = [a, b, c, 1, 2, 3],正如我们期望的一样。

请仔细思考上面的例子,并且确保完全理解变量初始化的模式,即:

_G518 = [a | _G587]

    = [a | [b | _G590]]

     = [a | [b | [c | _G593]]]

这种类型的模式就是append/3能够起作用的核心。而且,它展示出更为普通的主题:使用合一去构建结构。在核心层,append/3的递归调用会构建出嵌套模式的变量。当Prolog最终将

最里层的变量_G593和[1,2 ,3]合一,答案就会循环得出,就像是滚雪球一般。但是这仅仅是合一,不是其他什么魔法,就得出了结果。 

合并列表的使用

现在我们已经了解了append/3的工作机制,来看看如何实际运用它。

append/3的一个重要应用就是分割一个列表为两个连续的列表,比如:

?- append(X, Y, [a, b, c, d]).

X = []

Y = [a, b, c, d];

X = [a]

Y = [b, c, d];

X = [a, b]

Y = [c, d];

X = [a, b, c]

Y = [d];

X = [a, b, c ,d]

Y = [];

false

即,我们给出想要分割的列表(这里就是,[a, b, c, d])作为append/3的第三个参数,同时我们使用变量代表前两个参数。Prolog就会进行搜索,将两个变量初始化,并且合并后的值就是

第三个参数的列表,即分割列表为两个。而且,正如例子的结果显示,通过回溯,Prolog能够找到所有可能的组合值。

可以使用append/3定义许多其他有用的谓词。让我们思考一些例子。首先,我们可以定义一个谓词,找到列表的前缀,比如[a, b, c, d]的前缀是,[], [a], [a, b], [a, b, c]和[a, b, c, d],在

append/3的协助下,定义prefix/2很容易,其中两个参数都是列表,比如prefix(P, L)将会得出P是L的前缀,如下:

prefix(P, L) :- append(P, _, L).

这个定义就是说,列表P是列表L的前缀,如果列表L是由列表P和其他列表合并而成的(使用匿名变量代表我们不在乎其他列表具体是什么,我们只是知道这里有其他列表存在)。这个谓词可以

成功找出列表的前缀,并且通过回溯,可以找出所有的可能值:

?- prefix(X, [a, b, c, d]).

X = [];

X = [a];

X = [a, b];

X = [a, b, c];

X = [a, b, c, d];

false

类似地,我们可以定义一个谓词找出一个列表的后缀。比如,[a, b, c, d]的后缀是[], [d], [c, d], [b, c, d]和[a, b, c, d]。同样地,使用append/3可以方便地定义suffix/2,其中两个参数都是列表,

比如suffix(S, L)将会得出S是L的后缀,如下:

suffix(S, L) :- append(_, S, L).

这个定义就是说,列表S是列表L的后缀,如果列表L是由其他列表和列表S合并而成的,这个谓词可以成功找出列表的后缀,并且通过回溯,可以找出所有的可能值:

?- suffix(X, [a, b, c, d]).

X = [a, b, c, d];

X = [b, c, d];

X = [c, d];

X = [d];

X = [];

false

请确认你能够理解为什么答案是这样的顺序。

现在,可以十分轻松地定义一个谓词找出列表的子列表。[a, b, c, d]的子列表是,[], [a], [b], [c], [d], [a, b], [b, c], [c, d], [a, b, c], [b, c, d]和[a, b, c, d]。稍微想一下就能够知道一个列表的子

列表是这个列表的后缀的前缀,如图:

获取后缀: a, b, c, d, e, f, g, 

获取后缀:,m, n, o, p

结果:h, i, j, k, l

由于我们已经定义了列表前缀和后缀的谓词,所以子列表的谓词定义如下:

sublist(SubL, L) :- suffix(S, L), prefix(SubL, S).

即,SubL是L的子列表,如果存在列表S:S是L的后缀,并且SubL是S的前缀。这个谓词没有直接使用append/3,但是由于prefix/2和sufiix/2都使用了append/3,所以内部其作用的还是append/3。

时间: 2024-10-18 09:46:00

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

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 翻译 - 第五章 - 数字运算 - 第二节,数字运算与列表

内容提要 列表中的一些数字运算,累加器 尾递归调用 列表中的一些数字运算,累加器 关于数字运算最为重要的应用,可能是获取一些数据结构体的一些有用事实,比如列表.例如,知道列表的长度是很有用的.我们将会给出一些使用列表和数字运算的例子. 一个列表的长度是多少?这里有一个递归定义: 1. 空列表的长度为0. 2. 非空列表的长度为 1 + len(T),其中len(T)是非空列表的尾部. 这个定义在Prolog中很容易实现,以下是实现代码: len([], 0). len([_|T], N) :-

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

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

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中的列表,有如下的一些具体例子: [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 翻译 - 第四章 - 列表 - 第二节,列表成员

内容提要 本章主要介绍使用递归操纵列表的一个实际例子:判断一个元素是否在包含在一个列表中. 是时候介绍第一个Prolog中通过递归操纵列表的程序例子了.我们最感兴趣的事情之一是,某个对象是否是列表中的元素.所以,我们想写一个程序,当假设输入是一个对象X和一个列表L, 得出结果是X是否属于L.这个程序的名字通常是:member,是Prolog程序中使用递归操纵列表最简单的例子,如下: member(X, [X|T]). member(X, [H|T]) :- member(X, T). 这就是全部

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 翻译 - 第五章 - 数字运算 - 第四节,练习题和答案

练习题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)