Prolog编程:练习解答、子句形式转换与可移植性探讨
1. 部分练习解答
1.1 家庭关系定义
以下是一些家庭关系的可能定义:
is_mother(Mum) :- mother(Mum, Child).
is_father(Dad) :- father(Dad, Child).
is_son(Son):- parent(Par, Son), male(Son).
sister_of(Sis, Pers) :-
parent(Par, Sis), parent(Par, Pers),
female(Sis), diff(Sis, Pers).
granpa_of(Gpa, X) :- parent(Par, X), father(Gpa, Par).
sibling(Sl,S2) :-
parent(Par, Sl), parent(Par, S2), diff(Sl, S2).
这里使用了谓词
diff
来避免系统得出某人是自己的姐妹或兄弟姐妹的结论,但现阶段无法定义
diff
。
1.2 字符处理程序
以下程序会无限读取当前输入文件中的字符,并将其中的
a
替换为
b
后输出:
go :- repeat, get_char(C), deal_with(C), fail.
deal_with(a) :- !, put_char(b).
deal_with(X) :- put(X).
在
deal_with
的第一条规则中,“cut”(
!
)是必不可少的。
1.3 生成勾股数的程序
以下程序用于生成勾股数:
pythag(X, Y, Z) :-
intriple(X, Y, Z),
Sumsq is X*X + Y*Y, Sumsq is Z * Z.
intriple(X, Y, Z) :-
is_integer(Sum),
minus(Sum, X, Suml), minus(Suml, Y, Z).
minus(Sum, Sum, 0).
minus(Sum, Dl, D2) :-
Sum > 0, Suml is Sum - 1,
minus(Suml, Dl, D3), D2 is D3 + 1.
is_integer(0).
is_integer(N) :- is_integer(Nl), N is Nl + 1.
该程序使用谓词
intriple
生成可能的整数三元组
X
、
Y
、
Z
,然后检查该三元组是否为勾股数。
1.4 简单语法规则转换为Prolog
以下是将简单语法规则转换为Prolog的程序,假设规则中不包含带额外参数的短语类型、花括号内的目标、析取或“cut”:
?- op(1199,xfx,~>).
translate((Pl—>P2),(G1:-G2)) :-
left_hand_side(Pl,SO,S,Gl),
right_hand_side(P2,S0,S,G2).
left_hand_side(P0,S0,S,G) :-
nonvar(PO), tag(P0,S0,S,G).
right_hand_side((Pl,P2),S0,S,G) :-
right_hand_side(Pl,SO,Sl,Gl),
right_hand_side(P2,Sl,S,G2),
and(Gl,G2,G).
right_hand_side(P,SO,S,true) :-
islist(P),
append(P,S,SO).
right_hand_side(P,SO,S,G) :-
tag(P,S0,S,G).
tag(P,S0,S,G) :- atom(P), G =..[P,S0,S].
and(true,G,G) :-!.
and(G,true,G) :- !.
and(Gl,G2,(Gl,G2)).
islist([]) :- !.
islist([_|_]).
append([A|B],C,[A|D]) :- append(B,C,D).
append([],X,X).
在这个程序中,以
P
开头的变量代表语法规则中的短语描述,以
G
开头的变量代表Prolog目标,以
S
开头的变量代表Prolog目标的参数。
1.5 通用短语定义
通用版本的
phrase
定义如下:
phrase(Ptype,Words) :-
Ptype =.. [Pred|Args],
append(Args, [Words, []],Newargs),
Goal=.. [Pred|Newargs],
call(Goal).
其中
append
的定义与之前章节相同。
2. 子句形式程序清单
2.1 整体流程
将公式转换为子句形式的程序顶层如下:
translate(X) :-
implout(X,Xl), /* 阶段1 */
negin(Xl,X2), /* 阶段2 */
skolem(X2,X3,[]), /* 阶段3 */
univout(X3,X4), /* 阶段4 */
conjn(X4,X5), /* 阶段5 */
clausify(X5,Clauses), /* 阶段6 */
pclauses(Clauses). /* 打印子句 */
该程序定义了谓词
translate
,若给定目标
translate(X)
,其中
X
代表谓词演算公式,程序将输出该公式的子句表示。
2.2 运算符声明
需要进行以下运算符声明:
?- op(200,fx,~).
?- op(400,xfy,#).
?- op(400,xfy,&).
?- op(700,xfy,->).
?- op(700,xfy,<->).
需要注意运算符的优先级定义,特别是
<->
的优先级低于
#
和
&
。同时,要确保公式中的变量已按需重命名,避免出现名称冲突。
2.3 各阶段实现
2.3.1 移除蕴含关系(阶段1)
implout((P <-> Q),((P1 & Ql) # (~P1 & ~Q1))) :-
!, implout(P,Pl), implout(Q,Ql).
implout((P -> Q),(~P1 # Ql)) :-
!, implout(P,Pl), implout(Q,Ql).
implout(all(X,P),all(X,Pl)):- !, implout(P,Pl).
implout(exists(X,P),exists(X,Pl)):-!, implout(P,Pl).
implout((P & Q),(P1 & Ql)) :-
!, implout(P,Pl), implout(Q,Ql).
implout((P # Q),(P1 # Ql)) :-
!, implout(P,Pl), implout(Q,Ql).
implout((~P),(~Pl)) :- !, implout(P,Pl).
implout(P,P).
2.3.2 向内移动否定(阶段2)
negin((~P),Pl) :- !, neg(P,Pl).
negin(all(X,P),all(X,Pl)):- !, negin(P,Pl).
negin(exists(X,P),exists(X,Pl)) :-!, negin(P,Pl).
negin((P & Q),(P1 & Ql)) :-
!, negin(P,Pl), negin(Q,Ql).
negin((P # Q),(P1 # Ql)) :-
!, negin(P,Pl), negin(Q,Ql).
negin(P,P).
neg((~P),Pl):-!, negin(P,Pl).
neg(all(X,P),exists(X,Pl)):-!, neg(P,Pl).
neg(exists(X,P),all(X,Pl)):- !, neg(P,Pl).
neg((P & Q),(P1 # Ql)) :- !, neg(P,Pl), neg(Q,Ql).
neg((P # Q),(P1 & Ql)) :- !, neg(P,Pl), neg(Q,Ql).
neg(P,(~P)).
2.3.3 Skolem化(阶段3)
skolem(all(X,P),all(X,Pl),Vars) :-
!, skolem(P,Pl,[X|Vars]).
skolem(exists(X,P),P2,Vars):-
gensym(f,F),
Sk =..[F|Vars],
subst(X,Sk,P,Pl),
skolem(Pl,P2,Vars).
skolem((P # Q),(P1 # Ql),Vars) :-
!, skolem(P,Pl,Vars), skolem(Q,Ql,Vars).
skolem((P & Q),(P1 & Ql),Vars) :-
!, skolem(P,Pl,Vars), skolem(Q,Ql,Vars).
skolem(P,P,_).
此阶段使用了
gensym
和
subst
两个新谓词,
gensym
用于生成新的原子,
subst
用于替换公式中的变量。
2.3.4 向外移动全称量词(阶段4)
univout(all(X,P),Pl):- !, univout(P,Pl).
univout((P & Q),(P1 & Ql)) :-
!, univout(P,Pl), univout(Q,Ql).
univout((P # Q),(P1 # Ql)) :-
!, univout(P,Pl), univout(Q,Ql).
univout(P,P).
该定义假设此操作仅在前三阶段完成后进行,因此公式中不包含蕴含关系或存在量词。
2.3.5 分配
&
到
#
(阶段5)
conjn((P # Q),R) :-
conjn(P,Pl), conjn(Q,Ql),
conjnl((Pl # Ql),R).
conjn((P & Q),(P1 & Ql)) :-
!, conjn(P,Pl), conjn(Q,Ql).
conjn(P,P).
conjnl(((P & Q) # R),(P1 & Ql)) :-
!, conjn((P # R),P1), conjn((Q # R),Ql).
conjnl((P # (Q & R)),(P1 & Ql)) :-
!, conjn((P # Q),P1), conjn((P # R),Q1).
conjnl(P,P).
2.3.6 转换为子句(阶段6)
clausify((P & Q),C1,C2) :-
!, clausify(P,Cl,C3), clausify(Q,C3,C2).
clausify(P,[cl(A,B)|Cs],Cs) :-
inclause(P,A,[],B,[]),!.
clausify(_,C,C).
inclause((P # Q),A,A1,B,B1) :-
inclause(P,A2,Al,B2,Bl), inclause(Q,A,A2,B,B2).
inclause((~P),A,A,Bl,B) :-
!, notin(P,A), putin(P,B,Bl).
inclause(P,Al,A,B,B) :- notin(P,B), putin(P,A,Al).
notin(X,[X|_]) :-!, fail.
notin(X,[_|L]):-!, notin(X,L).
notin(X,[]).
putin(X,[],[X]):- !.
putin(X,[X|L],[X|L]):- !.
putin(X,[Y|L],[Y|Ll]):- putin(X,L,Ll).
2.4 打印子句
pclauses([]) :- !, nl, nl.
pclauses([cl(A,B) |Cs]) :-
pclause(A,B), nl, pclauses(Cs).
pclause(L,[]) :-
!, pdisj(L), write('.').
pclause([],L) :-
!, write(':-'), pconj(L), write('.').
pclause(Ll,L2) :-
pdisj(Ll),
write(':-'), pconj(L2), write('.').
pdisj([L]) :- !, write(L).
pdisj([L|Ls]) :- write(L), write(';'), pdisj(Ls).
pconj([L]) :- !, write(L).
pconj([L|Ls]) :- write(L), write(', '), pconj(Ls).
3. 编写可移植的标准Prolog程序
3.1 标准Prolog的可移植性
每次开发新的计算机硬件或操作系统时,通常需要编写新的程序来使Prolog对用户可用。不同的Prolog实现由不同的人在不同时间编写,它们可能会有不同的内置谓词名称和功能,这导致Prolog程序在不同实现上的兼容性问题。
为了解决这个问题,1995年制定了Prolog标准(ISO/IEC 13211 - 1),该标准精确规定了Prolog程序的定义和执行方式,包括一组内置谓词的规范。如果所有Prolog实现都完全符合标准,那么在不同实现上运行程序将不会有问题。
3.2 不同的Prolog实现
目前,很少有Prolog实现完全兼容标准Prolog。编写可移植Prolog程序的最佳方法是遵循标准。如果某个实现不提供标准谓词,但提供了可以用来定义该标准谓词的其他谓词,那么可以在单独的“兼容性文件”中定义这些谓词。
具体操作步骤如下:
1. 检查你的实现是否提供所需的标准谓词。
2. 如果没有提供,尝试使用其他可用的谓词来定义该标准谓词。
3. 将这些定义保存在一个单独的文件(兼容性文件)中。
4. 使用相同实现的用户在运行程序时,需要同时咨询兼容性文件。
5. 与标准完全兼容的实现用户可以直接运行程序,无需咨询兼容性文件。
6. 对于与标准不兼容的其他实现用户,可能需要编写自己的兼容性文件(可以参考已有的文件)。
以下是一个简单的表格总结不同情况:
| 实现情况 | 操作步骤 |
| ---- | ---- |
| 提供标准谓词 | 直接使用 |
| 不提供标准谓词 | 用其他谓词定义,保存到兼容性文件,运行时咨询该文件 |
| 与标准完全兼容 | 直接运行程序 |
| 与标准不兼容 | 编写自己的兼容性文件 |
mermaid流程图如下:
graph LR
A[开始] --> B{实现是否提供标准谓词}
B -- 是 --> C[直接使用]
B -- 否 --> D[用其他谓词定义]
D --> E[保存到兼容性文件]
E --> F{实现是否与标准完全兼容}
F -- 是 --> G[直接运行程序]
F -- 否 --> H[编写自己的兼容性文件]
H --> I[运行时咨询兼容性文件]
C --> J[结束]
G --> J
I --> J
通过以上内容,我们了解了Prolog编程中部分练习的解答、公式转换为子句形式的过程以及编写可移植标准Prolog程序的方法和注意事项。这些知识对于提高Prolog编程能力和程序的通用性具有重要意义。
4. 关键技术点分析
4.1 家庭关系定义中的谓词运用
在家庭关系定义中,使用了多个谓词来表示不同的家庭关系。例如
is_mother
、
is_father
、
is_son
等。这些谓词通过组合其他已有的谓词(如
mother
、
father
、
parent
、
male
、
female
)来实现特定的逻辑。
以
sister_of
谓词为例:
sister_of(Sis, Pers) :-
parent(Par, Sis), parent(Par, Pers),
female(Sis), diff(Sis, Pers).
该谓词表示
Sis
是
Pers
的姐妹,需要满足以下条件:
1.
Sis
和
Pers
有共同的父母
Par
。
2.
Sis
是女性。
3.
Sis
和
Pers
不是同一个人(通过
diff
谓词保证)。
这里的
diff
谓词虽然现阶段无法定义,但它起到了避免逻辑错误的重要作用,防止系统得出某人是自己姐妹的不合理结论。
4.2 字符处理程序中的“cut”运用
在字符处理程序中:
go :- repeat, get_char(C), deal_with(C), fail.
deal_with(a) :- !, put_char(b).
deal_with(X) :- put(X).
“cut”(
!
)在
deal_with(a)
规则中是必不可少的。当输入字符为
a
时,执行
deal_with(a)
规则,遇到“cut”后,会阻止回溯,确保不会再尝试其他规则。如果没有“cut”,当输入
a
时,程序可能会继续尝试
deal_with(X)
规则,导致逻辑混乱。
4.3 勾股数生成程序的逻辑
勾股数生成程序通过
intriple
谓词生成可能的整数三元组
X
、
Y
、
Z
,然后使用
pythag
谓词检查该三元组是否为勾股数。
pythag(X, Y, Z) :-
intriple(X, Y, Z),
Sumsq is X*X + Y*Y, Sumsq is Z * Z.
intriple
谓词的实现保证了所有可能的整数三元组最终都会被生成。它首先生成一个整数
Sum
作为
X
、
Y
、
Z
的和,然后使用
minus
谓词来生成
X
、
Y
、
Z
的值。
intriple(X, Y, Z) :-
is_integer(Sum),
minus(Sum, X, Suml), minus(Suml, Y, Z).
4.4 语法规则转换程序的结构
简单语法规则转换为Prolog的程序通过
translate
谓词将语法规则转换为Prolog目标。程序中使用了多个辅助谓词,如
left_hand_side
、
right_hand_side
、
tag
、
and
等。
translate((Pl—>P2),(G1:-G2)) :-
left_hand_side(Pl,SO,S,Gl),
right_hand_side(P2,S0,S,G2).
这些辅助谓词分别处理语法规则的左右两边,将短语描述转换为Prolog目标,并处理目标之间的逻辑关系。
4.5 子句形式转换程序的各阶段作用
公式转换为子句形式的程序分为六个阶段,每个阶段都有其特定的作用:
| 阶段 | 作用 |
| ---- | ---- |
| 移除蕴含关系 | 将公式中的蕴含关系(
->
和
<->
)转换为其他逻辑运算符(
#
和
&
) |
| 向内移动否定 | 将否定符号移动到公式内部,使否定只作用于原子公式 |
| Skolem化 | 消除存在量词,用Skolem函数或常量代替 |
| 向外移动全称量词 | 将全称量词移动到公式的最外层 |
| 分配
&
到
#
| 将公式转换为合取范式 |
| 转换为子句 | 将合取范式转换为子句形式 |
mermaid流程图如下:
graph LR
A[原始公式] --> B[移除蕴含关系]
B --> C[向内移动否定]
C --> D[Skolem化]
D --> E[向外移动全称量词]
E --> F[分配&到#]
F --> G[转换为子句]
G --> H[打印子句]
5. 总结与建议
5.1 总结
通过对Prolog编程中练习解答、子句形式转换和可移植性的探讨,我们了解到:
- 在编写Prolog程序时,要合理运用谓词和逻辑运算符,处理好各种逻辑关系,如家庭关系定义和语法规则转换。
- 对于复杂的程序,如公式转换为子句形式的程序,要按照特定的步骤进行处理,每个阶段都有其重要的作用。
- 编写可移植的Prolog程序时,要遵循标准Prolog的规范,处理好不同实现之间的兼容性问题。
5.2 建议
- 在实际编程中,多使用标准谓词,避免使用特定实现的内置谓词,以提高程序的可移植性。
- 对于复杂的逻辑处理,如公式转换,要仔细分析每个阶段的作用,确保程序的正确性。
- 在编写程序时,要注意代码的可读性和可维护性,添加必要的注释,方便自己和他人理解代码。
总之,掌握Prolog编程的这些知识和技巧,能够帮助我们编写更加高效、可靠和可移植的程序。
超级会员免费看
87

被折叠的 条评论
为什么被折叠?



