在这篇文章中,我们讨论了逻辑编程模型,它发现许多涉及事实和计算它们之间关系的应用领域。
简介。
逻辑编程是一种基于逻辑的计算机编程范式,程序语句在形式逻辑系统中表达关于问题的事实和规则。
逻辑编程语言的例子有,Prolog, Datalog, Alice, Ciao, ASP, Twelf。
它是基于术语和事实之间的关系规范,使用这些,加上推理的规则,可以从现有的事实中产生新的事实。
逻辑编程使人们能够通过执行查询来请求特定的推断事实,这些查询是由程序通过搜索条款并以各种方式组合得到的,当这些组合不能产生正确的结果时,就会进行回溯。
通过这种搜索,规则中的变量可能被绑定到术语上,这些绑定在回溯过程中必须被撤销。
这些术语可能是常量、结构(可能持有绑定/未绑定或其他结构)和变量。
这种绑定是通过统一
完成的
优点
逻辑编程表达了独立于实现的知识。
程序更加灵活、压缩,并且可以被理解。
它的使用范围超出了计算学科,因为它依赖于推理和表达式的精确含义。
它可以自然地改变,以支持特殊形式的知识,如元级或高阶知识。
逻辑编程使知识与它的使用分离,因此它是独立于机器架构。
缺点
用户(程序员)更喜欢操作性过强的机器操作程序。
没有办法表示状态变量的内置机制中的计算概念。
计算与推理。
为了理解逻辑编程,我们将首先看到计算和演绎之间的区别。
计算
为了计算,我们从给定的表达式开始,根据固定的指令集(程序)我们可以产生一个结果。
一个例子
15 + 26 -> (1 + 2 + 1)1 -> (3 + 1)1 -> 41.
计算是机械性的,不需要任何聪明才智。
扣除法
为了推导,我们从一个猜想开始,根据给定的规则*(公理,推理规则*),我们试图构建一个猜想的证明。
一个例子
an+bn≠cn对于n>2,......357年的努力工作......,QED。
逻辑编程模型。
逻辑编程是基于术语之间的命名关系、说明这些关系的事实以及来自既定事实的新事实的推理规则。
一个事实由关系的名称和它的术语的语法符号组成。
例如,一个二元关系,parent/2表示该关系被命名为parent
,管辖两个术语。
例如,一个单数关系,parent/1,管理一个术语。
具有二元关系的事实的例子
parent(john doe)
parent(one two)
一个查询可以是:?-parent(x, doe)其中x是一个未绑定的变量。
上述查询要求系统扫描并找到与查询相匹配的事实,然后显示匹配的变量值。
查询的主体是搜索的目标,如果目标中的变量有一个绑定,那么事实就与目标相匹配。因此,在这种情况下,目标会产生一个连续的绑定X = john。
传统上,变量名以大写字母开头,常数以小写字母开头。
与可能包含变量的事实不同,查询不需要包含变量,这个特征对于表达像*equal(X, X)*这样的事实很有用,它说明X的任何值都等于X。
一个规则的例子
grandparent(X, Z) :- parent(X, Y), parent(Y, Z)
:-是规则的头部,下面的部分是规则的主体。
上面的规则意味着,在建立了事实parent(X, Y)和parent(Y, Z)的值X, Y, Z之后,我们推断出事实grandparent(X, Z)。
我们也可以把它解释为用目标列表parent(X, Y), parent(Y, Z)代替目标grandparent(X, Z),用于任何X, Y, Z的绑定。
推理机制。
推理技术如下。
我们有一个查询*?-grandparent(john, X)*,该查询要求找到john是其祖父母的X,并将grandparent(john, X)作为初始目标。
系统将试图找到/推断出X的某些绑定的目标。
Grandparent(X, Z) :- parent(X, Y), parent(Y, Z)
被用作推断的规则。
因此,我们用新的变量做一个副本
这就是所谓的统一
过程,我们将在下一节讨论。
我们用新的目标列表
parent
(
john,
Y1), parent
(Y
1, X
)
替换目标grandparent(j
ohn, X)
。
然后我们通过使用绑定X=doe将parent(john, Y1
)
与事实parent(john doe)
统一。
查询的答案是grandparent(john, doe)。
统一。
最简单的统一形式涉及常量和变量,正如我们所看到的,但是还有三种可能性,即。
- 两个常数之间的统一,如果两个常数相等,则成功,否则失败。
- 两个非绑定变量之间的统一,其中头部的变量与目标中的变量绑定,则成功。
- 非绑定变量和常量之间的统一,如果常量与变量绑定,则统一成功。
结构、列表和集合的统一。
结构是一个由固定数量的组件组成的命名列表,也叫漏斗。
例如,times(2, X)是一个结构的例子,其名称为times,组件为2和X
。
结构只能与其他变量或结构统一。
当两个结构的名称和组件的数量相等时,它们的统一规则与(head, goal)对相同,而且这些组件应该可以通过找到适当的绑定而成对统一。
列表的统一遵循与结构相同的规则。
一个例子
[X plus(1, X), times(2, X)] 和 [a, plus(1, Y), times(2, a)] 被 X = a 和 Y = a 统一。
集合的统一是这样的。
一个例子
我们有一个事实pa({john|jane}, doe),它总结为pa(john, doe),pa(jane, doe)。
目标列表是;
[ pa(X,doe) ?= pa({john|john}, doe )],<<( "X",X)
,
它产生两个可能的统一结果;
*[ pa(john, doe) ?= pa({john|jane}, doe )],<<( "X",john)
[pa(jane, doe)
?
= pa({john|jane}, doe )],<<( "X",jane)
*
我们使用统一指令,因为这种乘法可能在不同程度上发生在头部和第一目标中的每个术语对。
我们的想法是每次对一个术语对进行统一,而不是对
整个(头目标)对
进行统一
。
[pa(→X, doe) ? = pa(→{john|jane}, doe)],<<("X",X)
之后,要统一的术语对用前面的→标记成
[pa(john, →doe) ?= pa({john|jane}, →doe)],<<("X",john)
[pa(→X, doe) ?= pa({john|→jane}, doe)], <<("X",
X)
实现一个统一。
以下几组C语言的数据类型被用于构建术语。
struct variable{
char *name;
struct term *term;
}
struct structure{
char *functor;
int arity;
struct term **components;
}
typedef enum{Is_Constant, Is_Variable, Is_Structure} Term_Type;
typedef struct term{
Term_Type type;
union{
char *constant; //Is_Constant
struct variable variable; //Is_variable
struct structure structurel //Is_Structure
} term;
} Term;
我们将Term定义为常量、变量和结构的判别联合体。
常量用一个指向其值的指针表示,并以字符串形式存储。
变量也用一个指针表示,并保留它的名字。
对于绑定的变量,指针指向该变量所绑定的术语。
非绑定的变量由一个空指针标记。
一个结构被实现为一个记录,其中有三个字段,分别指定函数、组件数量和一个指向N个组件的数组的指针,这对分配很有用。
两个术语的统一
int unifyTerms(Term *goal_arg, Term *head_arg){
//Handle bound variables
Term *goal = deref(goal_arg);
Term *head = deref(head_arg);
if(goal->type == Is_Variable || head->type == Is_Variable){
return unify_unbound_variable(goal, head);
}else{
return unify_non_variables(goal, head);
}
}
统一将区分涉及一个变量的统一和涉及两个非变量术语的统一。
上述*unify_terms()*例程将通过取消对术语的引用来处理任何绑定的变量。
照顾非绑定变量
Term *deref(Term *t){
while(t->type == Is_Variable && t->term.variable.term != 0){
t = t->term.variable.term;
}
return t;
}
当一个术语在取消引用后仍然是一个变量时,它就是未绑定的。
函数deref()
会
跟踪一个指针链,直到它找到一个非变量或指针为零的变量。
至少有一个非绑定变量的统一。
int unify_unbound_variable(Term *goal, Term *head){
//handling identical values
if(goal == head)
//trivial unification of identical variables
else{
//bind unbound variable to the other term
if(head->type == Is_Variable){
trail_binding(head);
head->term.variable.term = goal;
}else{
trail_binding(goal);
goal->term.variable.term = head;
}
}
//unification succeeds
return 1;
}
该例程确定了一种特殊情况(即绑定和未绑定的变量都是相同的)。这种统一是微不足道的。
作为变量的术语被绑定到另一个术语上,这个术语被trail_binding函数注册,这样它就可以在以后的回溯过程中被解除绑定。
统一两个非变量
int unify_non_variables(Term *goal, Term *head){
//handling differnt type terms
if(goal->type != head->type)
return 0;
switch(goal->type){
case Is_Constant: //both constants
return unify_constants(goal, head);
case Is_Structure:
return unify_structures(&goal->term.structure, &head->term.structure);
}
}
如果两个术语是不同的类型,那么统一就会失败,否则就会调用适合该数据类型的例程。
统一两个常量
int unify_constants(Term *goal, Term *head){
return strcmp(goal->term.constant, head->term.constant) == 0;
}
当两个常量的值的字符串比较成功时,它们就被统一了。
统一两个结构
int unify_structures(struct structure *s_goal, struct structure *s_head){
int counter;
if(s_goal -> arity( !- s_head->arity || strcmp(s_goal->functor, s_head->arity; counter++){
if(!unify_terms(s_goal->components[counter[, s_head->components[counter]))
return 0;
}
return 1;
}
首先检查这两个结构是否有相同的算数,然后为每一对术语调用unify_terms。
如果它成功了,那么这两个结构的统一就成功了,否则就失败了。
统一两个未绑定的变量。
变量之间是相互关联的,因此它们都是未绑定的,当其中一个变量被绑定到一个常数或第三个变量时,另一个也会被绑定。
这是通过将其中一个变量绑定到另一个变量上,并在使用前取消对任何变量的引用来实现的,以便发现它实际上被绑定到什么。
问题是,将一个未绑定的变量B绑定到一个未绑定的变量A上,会导致一个未绑定的变量B。
图片显示了四个变量-变量绑定的序列的预期结果,首先是B-A,C-B,D-B和E-C。
图片显示了实际的绑定情况。
绑定B-A的结果是绑定B-A,但是绑定C-B开始时,取消了对B和C的引用,却发现B实际上是A,因此在实现中C被绑定到A而不是B,这种情况发生在绑定D-B和E-C上。
总结。
在逻辑编程中,用户指定问题陈述,逻辑编程系统将通过追踪给定的信息找到问题的解决方案。
Prolog是使用最广泛的逻辑编程语言,它包括一个模式匹配机制,一个回溯策略,用于搜索可能的解决方案和统一的数据结构,程序是由这些数据结构构建的。
参考文献.
- 逻辑和Prolog : Richard Spencer-Smith.
- 加州州立大学萨克拉门托分校的逻辑编程课件。
- 现代编译器设计第二版 Dick Grune Kees van Reeuwijk Henri E. Bal Ceriel J.H. Jacobs Koen Langendoen.