转载自:https://mp.weixin.qq.com/s/5tNIRd0XRtuhO-6zcQ3_UA
本章有两个主要目标:
1.讨论Prolog中的数据库操作。
2.讨论内置谓词,这些谓词使我们将问题的所有解答收集到一个表中
1 数据库操作
Prolog具有四个数据库操作命令:assert,retract,asserta和assertz。让我们看看如何使用它们。假设我们从一个空的数据库开始。因此,如果我们给出命令:
?-listing.
然后Prolog只会回答true;列出(当然)是空的。
假设我们现在给出以下命令:
?- assert(happy(mia)).
这个成功(assert / 1命令始终成功)。但是重要的不是它是否成功,而是它对数据库的副作用。因为如果我们现在给出命令
?- listing.
我们得到
happy(mia).
也就是说,数据库不再为空:它现在包含我们声明的事实。
假设然后再发出四个声明命令:
?- assert(happy(vincent)).
true
?- assert(happy(marcellus)).
true
?- assert(happy(butch)).
true
?- assert(happy(vincent)).
true
然后要求列出:
?-listing.
happy(mia).
happy(vincent).
happy(marcellus).
happy(butch).
happy(vincent).
true.
我们声明的所有事实现在都在知识库中。请注意,happy(vincent)在知识库中有两次。正如我们两次声明的那样,这似乎是明智的。
我们一直在进行的数据库操作改变了谓词happy / 1的含义。更一般而言,数据库操作命令使我们能够在运行程序时更改谓词的含义。其定义在运行时发生变化的谓词称为动态谓词,与我们之前处理过的静态谓词相反。大多数Prolog解释器坚持认为,我们明确声明希望动态的谓词。我们很快将研究一个涉及动态谓词的示例,但首先让我们完成对数据库操作命令的讨论。
到目前为止,我们仅将事实声明到数据库中,而且我们还可以声明新规则。假设我们要主张一个规则,即每个happy的人都naive。也就是说,假设我们要声明:
naive(X): happy(X).
我们可以这样做,如下所示:
assert((naive(X):- happy(X))).
请注意此命令的语法:我们要声明的规则放在一对括号中。如果现在我们要求表,我们得到:
happy(mia).
happy(vincent).
happy(marcellus).
happy(butch).
happy(vincent).
naive(A):-
happy(A).
既然我们知道了如何将新信息声明到数据库中,我们还应该学习如何在不再需要信息时将其删除。 assert / 1有一个相反操作的谓词,即retract / 1。例如,如果我们通过给出以下命令直接继续上一个示例:
?- retract(happy(marcellus)).
然后列出数据库,我们得到:
happy(mia).
happy(vincent).
happy(butch).
happy(vincent).
naive(A) :-
happy(A).
也就是,事实happy(marcellus)已被删除。
假设我们继续说
?- retract(happy(vincent)).
然后要求列出。我们得到:
happy(mia).
happy(butch).
happy(vincent).
naive(A) :-
happy(A).
请注意,第一次出现的happy(vincent)并且仅仅是第一次出现的被删除。
要删除所有assert定义的谓词happy / 1,我们可以使用一个变量:
?- retract(happy(X)).
X = mia ;
X = butch ;
X = vincent.
清单显示该数据库现在为空,但规则naive(A) :- happy(A).除外
?- listing.
naive(A):-
happy(A).
如果我们想更好地控制所声明的资料的放置位置,则有assert / 1的两个变体,即:
1.assertz将声明的资料放在数据库的末尾。
2.asserta将声明的资料放在数据库的开头。
例如,假设我们从一个空的数据库开始,然后给出以下命令:
assert( p(b) ), assertz( p(c) ), asserta( p(a) ).
然后清单显示我们现在拥有以下数据库:
?- listing.
p(a).
p(b).
p(c).
true.
数据库操作是一种有用的技术。这对于将结果存储到计算中特别有用,这样,将来如果我们需要提出相同的问题,就无需重做工作:我们只需查找已断定的事实。这种技术称为记忆或缓存,在某些应用程序中,它可以大大提高效率。这是使用此技术的简单示例:
:- dynamic lookup/3.
add_and_square(X,Y,Res):-
lookup(X,Y,Res), !.
add_and_square(X,Y,Res):-
Res is (X+Y)*(X+Y),
assert(lookup(X,Y,Res)).
这个程序做什么?基本上,它使用两个数字X和Y,将X加到Y,然后对结果求平方。例如,我们有:
?- add_and_square(3,7,X).
X = 100.
但是重要的一点是:它是如何做到的?首先,请注意,我们已将lookup / 3声明为动态谓词。我们需要这样做,因为我们计划在运行时更改lookup / 3的定义。第二,请注意,有两个子句定义add_and_square / 3。第二个子句执行所需的算术计算,并使用谓词lookup / 3将结果声明到Prolog数据库中(即,它缓存结果)。第一个子句检查Prolog数据库,以查看过去是否已经进行过计算。如果已经存在,则程序仅返回结果,并且截断会阻止其进入第二子句。
?- add_and_square(3,4,Y).
Y = 49
如果现在我们要求一个清单,我们会看到数据库现在包含
lookup(3,7,100).
lookup(3,4,49).
如果我们以后要求Prolog将3和4相加并平方,它将不再执行计算。相反,它将仅返回先前计算的结果。
问题:当我们不再需要这些新事实时,我们该如何删除它们?毕竟,如果我们给出命令
?- retract(lookup(X,Y,Z)).
Prolog将一一介绍所有事实,并询问我们是否要删除它们!但是有一种更简单的方法。只需使用命令
?-retract(lookup(_,_,_)).
这将从数据库中删除所有有关lookup / 3的事实。
在结束我们对数据库操作的讨论时,请注意。尽管这是一种有用的技术,但数据库操作会导致脏的,难以理解的代码。如果您在具有大量回溯功能的程序中大量使用它,那么了解正在发生的事情可能是一场噩梦。这是Prolog的非声明性,非逻辑性的功能,应谨慎使用。
2 收集解
一个查询可能有很多解答。例如,假设我们正在使用数据库
child(martha,charlotte).
child(charlotte,caroline).
child(caroline,laura).
child(laura,rose).
descend(X,Y) :- child(X,Y).
descend(X,Y) :- child(X,Z),
descend(Z,Y).
然后,如果我们提出查询
descend(martha,Y).
有四种解答(即X =charlotte,X =caroline,X =laura和X =rose)。
但是,Prolog会一一生成这些解答。有时,我们希望拥有查询的所有解答,并且希望它们以整洁,可用的形式交给我们。 Prolog具有执行此操作的三个内置谓词:findall,bagof和setof。从本质上讲,所有这些谓词都会收集查询的所有解答并将它们放在一个表中,但是如我们所见,它们之间存在重要区别。
findall/3谓词
查询
?- findall(Object,Goal,List).
产生满足目标Goal的所有对象Object的表List。通常,Object只是一个变量,在这种情况下,查询可以理解为:给我一个包含满足Goal的所有Object实例化的表。
这是一个例子。假设我们正在使用上面的数据库(即有关child和子孙定义的信息)。然后,如果我们构成查询
?- findall(X,descend(martha,X),Z).
我们需要一个表Z,其中包含满足descend(martha,X)的X的所有值。 Prolog会回应
X = _7489
Z = [charlotte,caroline,laura,rose]
但是Object不必一定是变量,它可能是一个复合项,只包含一个也出现在Goal中的变量。例如,我们可能决定要从Martha / 1中建立一个仅对玛莎后裔适用的谓词。我们可以通过查询来做到这一点:
?- findall(fromMartha(X),descend(martha,X),Z).
也就是说,我们要获取一个表Z,其中包含满足目标descend(martha,X)的fromMartha(X)的所有实例。 Prolog会回应
Z = [fromMartha(charlotte), fromMartha(caroline), fromMartha(laura), fromMartha(rose)].
如果我们询问以下查询会怎样?
?- findall(X,descend(mary,X),Z).
由于在知识库中没有针对目标descend(mary,X)的解决方案。 findall / 3返回一个空表。
请注意,findall / 3的前两个参数通常具有(至少)一个公共变量。使用findall / 3时,我们通常想知道Prolog为目标中的某些变量找到了什么解答,并且通过将它们构建到findall / 3的第一个参数中来告诉Prolog我们对目标中的哪些变量感兴趣
但是,您可能会遇到这样的情况,尽管前两个参数不共享任何变量,但findall / 3会发挥作用,例如,如果您对究竟是谁是玛莎的后代不感兴趣,而只对玛莎有多少后代感兴趣,您可以使用以下查询来找出:
?- findall(Y,descend(martha,X),Z), length(Z,N).
bagof / 3谓词
findall / 3谓词很有用,但在某些方面还很粗糙。例如,假设我们构成查询
?- findall(Child,descend(Mother,Child),List).
我们得到回应
List = [charlotte, caroline, laura, rose, caroline, laura, rose, laura, rose|...].
现在,这是正确的,但是有时候,如果我们为Mother的每个不同实例都有一个单独的表,这将很有用。
这就是bagof / 3所所做的。如果我们提出查询
?- bagof(Child,descend(Mother,Child),List).
我们得到了回应
Mother = caroline,
List = [laura, rose] ;
Mother = charlotte,
List = [caroline, laura, rose] ;
Mother = laura,
List = [rose] ;
Mother = martha,
List = [charlotte, caroline, laura, rose].
也就是说,bagof / 3比的findall / 3更细化。它使我们有机会以更结构化的方式提取所需的信息。此外,在特殊语法(即^)的帮助下,bagof / 3也可以完成与findall / 3相同的工作:
?- bagof(Child,Mother^descend(Mother,Child),List).
这就是说:给我一个有关Child的所有值的表,例如descend(Mother,Child),并将结果放入表中,但不必担心会为每个Mother值生成单独的表。因此,构成此查询将产生:
List = [charlotte, caroline, laura, rose, caroline, laura, rose, laura, rose|...].
请注意,这正是findall / 3会给我们的响应。不过,如果您要进行这种查询(通常是这样),则使用findall / 3会更简单,因为这样就不必费心用^写下条件。
findall / 3和bagof / 3之间有一个重要的区别,即,如果不满足在其第二个参数中指定的目标,bagof / 3将失败(请记住,在这种情况下,findall / 3返回空表)。因此查询bagof(X,descend(mary,X),Z)得出false。
最后一句话。再次考虑查询
?- bagof(Child,descend(Mother,Child),List).
正如我们在上面看到的,这有四个解答。但是,Prolog再一次生成它们。如果我们可以将它们全部收集到一个表中,这会很好吗?
而且我们可以。最简单的方法是使用findall / 3。查询
?- findall(List,
bagof(Child,descend(Mother,Child),List),
Z).
将bagof / 3的所有回复收集到一个表中:
Z = [[laura, rose], [caroline, laura, rose], [rose], [charlotte, caroline, laura, rose]].
另一种方法是使用bagof / 3:
?- bagof(List,Child^Mother^bagof(Child,descend(Mother,Child),List),Z).
Z = [[laura,rose],[caroline,laura,rose],[rose],[charlotte,caroline,laura,rose]]
这可能不是您经常需要做的事情,但是它确实显示了这些谓词所提供的灵活性和强大功能。
setof / 3谓词
setof / 3谓词与bagof / 3基本上相同,但是有一个有用的区别:它包含的表是有序的,并且不包含冗余(即,没有表包含重复的项)。
例如,假设我们有以下数据库
age(harry,13).
age(draco,14).
age(ron,13).
age(hermione,13).
age(dumbledore,60).
age(hagrid,30).
现在假设我们需要一个年龄表,该年龄表记录在数据库中。我们可以通过查询来做到这一点:
?-findall(X,age(X,Y),Out).
Out = [harry,draco,ron,hermione,dumbledore,hagrid].
但也许我们希望得到一个有序的表。我们可以通过以下查询来实现:
?- setof(X,Y^age(X,Y),Out).
(请注意,就像bagof / 3一样,我们必须告诉setof / 3不要为每个Y值生成单独的表,并且再次使用^符号来完成此操作。)此查询产生:
Out = [draco,dumbledore,hagrid,harry,hermione,ron]
请注意,该表按字母顺序排列。
现在假设我们有兴趣收集数据库中记录的所有年龄。当然,我们可以使用以下查询执行此操作:
?- findall(Y,age(X,Y),Out).
Out = [13,14,13,13,60,30]
但是,此输出相当混乱。它是无序的并且包含重复项。通过使用setof / 3,我们可以以更整洁的形式获得相同的信息:
?- setof(Y,X^age(X,Y),Out).
Out = [13,14,30,60]
在收集解答时,这三个谓词为我们提供了极大的灵活性。对于许多目的,我们需要的只是findall / 3,但是如果需要更多,bagof / 3和setof / 3都在待用状态。但是请记住,一方面findall / 3与另一方面bagof / 3和setof / 3之间存在重要区别:如果目标没有解答,findall / 3将返回一个空表,而bagof / 3和setof / 3在这种情况下,将失败。
3 练习
练习11.1 假设我们从一个空的数据库开始。然后我们给出命令:
assert(q(a,b)), assertz(q(1,2)), asserta(q(foo,blug)).
数据库现在包含什么?
We then give the command:
retract(q(1,2)), assertz( (p(X) :- h(X)) ).
数据库现在包含什么?
练习11.2 假设我们有以下数据库:
q(blob,blug).
q(blob,blag).
q(blob,blig).
q(blaf,blag).
q(dang,dong).
q(dang,blug).
q(flab,blob).
Prolog对查询的答复是什么:
findall(X,q(blob,X),List).
findall(X,q(X,blug),List).
findall(X,q(X,Y),List).
bagof(X,q(X,Y),List).
setof(X,Y^q(X,Y),List).
练习11.3 编写一个谓词sigma / 2,它采用整数n> 0并计算从1到n的所有整数之和。例如:
?- sigma(3,X).
X = 6.
?- sigma(5,X).
X = 15.
编写谓词,以便将结果存储在数据库中(每个值在数据库中永远不应有多个条目),并尽可能重用。例如,假设我们进行以下查询:
?- sigma(2,X).
X = 3.
?- listing(sigmares/2).
:- dynamic sigmares/2.
sigmares(0, 0).
sigmares(1, 1).
sigmares(2, 3).
true.
那我们继续一下问
?- sigma(3,X).
Prolog不应计算所有新数据,而应从数据库中获取sigma(2,3)的结果,并仅将其加3。然后应该回答:
X = 6.
?- listing(sigmares/2).
:- dynamic sigmares/2.
sigmares(0, 0).
sigmares(1, 1).
sigmares(2, 3).
sigmares(3, 6).
true.
4 实践环节
请尝试以下两个编程练习:
1.集合可被视为不包含任何重复元素的表。例如,[a,4,6]是一个集合,但[a,4,6,a]不是一个集合(因为它包含两次出现的a)。编写一个Prolog程序subset / 2,当第一个自变量是第二个自变量的子集时(即,第一个自变量的每个元素都是第二个自变量的成员时),将满足该要求。例如:
?- subset([a,b],[a,b,c])
true
?- subset([c,b],[a,b,c])
true
?- subset([],[a,b,c])
true
您的程序应该能够通过回溯生成输入集的所有子集。例如,如果您将其作为输入
?- subset(X,[a,b,c])
它应该依次生成[a,b,c]的所有八个子集。
2.使用您刚编写的子集谓词和findall/3,编写一个谓词powerset/2,将该谓词powerset/2的第一个参数作为一个集合,并将该集合作为第二个参数返回。 (该集合是其所有子集的集合。)例如:
?- powerset([a,b,c],P)
将返回
P = [[],[a],[b],[c],[a,b],[a,c],[b,c],[a,b,c]]
集合是否以其他顺序返回也没关系。例如,
P = [[a],[b],[c],[a,b,c],[],[a,b],[a,c],[b,c]]
也可以