用于扩展模块谓词的 Prolog 设计模式
Prolog Design Pattern to extend module predicates
假设我们有下面的 familytree
模块(简单示例):
:- module(familytree, [
father/2,
mother/2,
%[...]
]).
father(X,Y) :- male(X),parent(X,Y).
father(unknown, _) :- male(unknown).
mother(X,Y) :- female(X),parent(X,Y).
mother(unknown, _) :- female(unknown).
sister(X,Y) :- female(X),parent(Z,X),parent(Z,Y), X \= Y.
%[... other relation predicates ... ]
我想将此模块谓词与不同的 "dbs" 一起使用,例如:
:- module(familytree_xyz, []).
male(james).
male(fred).
male(mike).
female(betty).
female(sandra).
parent(james, fred).
parent(betty, fred).
或:
:- module(familytree_simpson, []).
male(homer).
male(bart).
female(marge).
female(lisa).
parent(homer, bart).
%[...]
我需要:
- 在运行时而不是编译时选择数据库。
- 同时使用一个或多个数据库。
- 扩展数据库,例如。创建一个“familytree_simpson_extended”数据库模块,其他辛普森家族成员扩展“familytree_simpson”数据库模块(见上例)
- 符合 swi-prolog。
目前,我尝试使用 term_expansion/2
、discontiguous/1
、multifile/1
、dynamic/1
和 thread_local/1
指令,但是:
term_expansion/2
似乎只能在编译时使用,
discontiguous/1
,multifile/1
,未改编,
- 序言中的动态数据库被视为“邪恶”的做法,但是很多包和库使用它的(
pengines
,broadcast
模块, http
库,例如)。
thread_local/1
没有很好的文档记录,似乎不常用于 prolog 源代码 (swi-prolog)。
通过使用动态谓词,我将之前的代码更新如下:
%familytree.pl
:- module(familytree, [
familytree_cleanup_db/0,
familytree_use_db/1,
%[... previous declarations ...]
]).
dynamic male/1, female/1, parent/2.
familytree_cleanup_db :-
retractall(male/1),
retractall(female/1),
retractall(parent/2).
familytree_use_db(ModuleName) :-
assert(male(X) :- ModuleName:male(X)),
assert(female(X) :- ModuleName:female(X)),
assert(parent(X,Y) :- ModuleName:parent(X,Y)).
%[... previous predicates ...]
并且:
%main.pl
% use familytree tool predicates
:- use_module(familytree).
%load all familytree dbs at compile time.
:- use_module(familytree_xyz).
:- use_module(familytree_simpson).
:- use_module(familytree_simpson_extended).
main_xyz:-
familytree_cleanup_db,
familytree_use_db(familytree_xyz),
process.
main_simpson_all :-
familytree_cleanup_db,
familytree_use_db(familytree_simpson),
familytree_use_db(familytree_simpson_extended),
process.
process :-
findall(X, father(X,_), Xs),
write(Xs).
并且可以使用不同的数据库,如下所示:
?- main_simpson_all.
[homer,homer,abraham]
true.
?- main_xyz.
[james]
true.
所以,对于 post 的长度感到抱歉。问题:
使用此动态谓词解决方案要考虑的标准是什么,pros/cons?这是一个好的解决方案吗?
什么是最佳实践/特定的设计模式,以便序言在干净/健壮的代码中做到这一点?**
使用 thread_local/1
代替 dynamic/1
并封装对新线程的调用以避免清理数据库有何意义?
由于源数据库显然在您的用例中起着重要作用,我建议在您的定义中明确其专用标识符,以便始终清楚您实际引用的是哪个家庭源:
db_male(xyz, james).
db_male(simpsons, bart).
db_female(xyz, betty).
db_female(simpsons, marge).
db_parent_of(xyz, james, fred).
因此,您基本上拥有 public 和多文件 db_male/2
、db_female/2
、db_parent_of/3
谓词。
自包含模块可以使用自己的源知识库扩展现有定义,在第一个参数中明确说明。这就是 term_expansion/2
等可能对您有所帮助的地方:由于数据库名称在每个单独的模块中都是相同的,因此您可以编写扩展代码来扩充 male/1
、female/1
的特定于模块的定义等使用合适的 db
参数并将其重写为 db_male/2
等。请注意,此重写只需要在 编译时 发生。在 运行 时,您可以提供您选择的任何数据库作为这些谓词的第一个参数。
female/1
、male/1
的全局定义也很明显:
male(M) :- db_male(_, M).
另请注意,我使用 parent_of/2
之类的名称来明确哪个参数是什么。
assertz/1
可用于在需要时动态扩充每个单独的数据库,再次明确提供名称。但是,为了干净和健壮的代码,我会在编译时尽可能多地做。
扩展我的评论,Logtalk 解决方案很简单。首先,用家庭关系谓词定义一个根对象:
:- object(familytree).
:- public([
father/2, mother/2,
sister/2, brother/2
]).
:- public([
parent/2,
male/1, female/1
]).
father(Father, Child) :-
::male(Father),
::parent(Father, Child).
mother(Mother, Child) :-
::female(Mother),
::parent(Mother, Child).
sister(Sister, Child) :-
::female(Sister),
::parent(Parent, Sister),
::parent(Parent, Child),
Sister \== Child.
brother(Brother, Child) :-
::male(Brother),
::parent(Parent, Brother),
::parent(Parent, Child),
Brother \== Child.
:- end_object.
请注意,male/1
、female/1
和 parent/2
定义的查找从 self 开始,即在对象中,数据库,它将接收有关家庭关系的查询。从您的示例代码派生的一个示例是:
:- object(simpsons,
extends(familytree)).
male(homer).
male(bart).
female(marge).
female(lisa).
parent(homer, bart).
parent(homer, lisa).
parent(marge, bart).
parent(marge, lisa).
:- end_object.
示例查询可以是:
?- simpsons::parent(homer, Child).
Child = bart ;
Child = lisa.
您可以根据需要添加任意数量的家族数据库,同时加载它们,并随意定义它们的专业化。例如:
:- object(simpsons_extended,
extends(simpsons)).
male(Male) :-
^^male(Male).
male(abe).
male(herb).
female(Male) :-
^^female(Male).
female(gaby).
female(mona).
parent(Parent, Child) :-
^^parent(Parent, Child).
parent(abe, homer).
parent(abe, herb).
parent(gaby, herb).
parent(mona, homer).
:- end_object.
这个解决方案可以满足您的所有要求。 SWI-Prolog 是受支持的 Prolog 编译器之一。您可以使用其安装程序安装 Logtalk。或者,对于 SWI-Prolog,您只需键入:
?- pack_install(logtalk).
更新
在您对此解决方案的评论中,您询问了有关将数据库注入家谱对象逻辑的问题。这很容易,但也需要不同的方法。首先定义familytree
为:
:- object(familytree).
:- public([
father/2, mother/2,
sister/2, brother/2
]).
:- public([
parent/2,
male/1, female/1
]).
:- multifile([
parent/2,
male/1, female/1
]).
father(Father, Child) :-
male(Father),
parent(Father, Child).
mother(Mother, Child) :-
female(Mother),
parent(Mother, Child).
sister(Sister, Child) :-
female(Sister),
parent(Parent, Sister),
parent(Parent, Child),
Sister \== Child.
brother(Brother, Child) :-
male(Brother),
parent(Parent, Brother),
parent(Parent, Child),
Brother \== Child.
:- end_object.
请注意,这是另一种选择,我们将 male/1
、female/1
和 parent/2
称为本地谓词,但它们也被声明为多文件谓词。现在我们需要在 familytree
对象中 "inject" 一个家庭数据库:
:- category(simpsons).
:- multifile([
familytree::male/1,
familytree::female/1,
familytree::parent/2
]).
familytree::male(homer).
familytree::male(bart).
familytree::female(marge).
familytree::female(lisa).
familytree::parent(homer, bart).
familytree::parent(homer, lisa).
familytree::parent(homer, maggie).
familytree::parent(marge, bart).
familytree::parent(marge, lisa).
familytree::parent(marge, maggie).
:- end_category.
用法示例(假设 familytree.lgt
和 simpsons.lgt
个文件):
?- {familytree, simpsons}.
...
yes
几个示例查询:
?- familytree::parent(homer, Child).
Child = bart ;
Child = lisa ;
Child = maggie.
?- familytree::male(Male).
Male = homer ;
Male = bart.
?- familytree::father(Father, Child).
Father = homer,
Child = bart ;
Father = homer,
Child = lisa ;
Father = homer,
Child = maggie ;
false.
另一种方法是使用 Prolog dicts。它们由 SWI-Prolog 引入,自 1.3.0 版以来,它们也可用于 Jekejeke Prolog。如果不需要接收器,可以简单地使用下划线。
文件simpson.pl:
:- module(simpson, [gender/3, parent/3]).
:- reexport(familytree).
_.gender(homer) := male.
_.gender(bart) := male.
_.gender(marge) := female.
_.gender(lisa) := female.
_.parent(homer) := bart.
_.parent(homer) := lisa.
_.parent(homer) := maggie.
_.parent(marge) := bart.
_.parent(marge) := lisa.
_.parent(marge) := maggie.
文件xyz.pl:
:- module(xyz, [gender/3, parent/3]).
:- reexport(familytree).
_.gender(james) := male.
_.gender(fred) := male.
_.gender(mike) := male.
_.gender(betty) := female.
_.gender(sandra) := female.
_.parent(james) := fred.
_.parent(betty) := fred.
文件familytree.pl:
:- module(familytree, [father/3]).
M.father(X) := Y :-
male = M.gender(X),
Y = M.parent(X).
要使家谱在 simpson 和 xyz 中也可见,请使用 reexport/1。这允许向 simpson 或 xyz 发送消息,但仍然会处理 familytree 中的方法。这是一个例子 运行:
Welcome to SWI-Prolog (threaded, 64 bits, version 7.7.19)
SWI-Prolog comes with ABSOLUTELY NO WARRANTY. This is free software.
?- Y = simpson{}.father(X).
Y = bart,
X = homer ;
Y = lisa,
X = homer ;
Y = maggie,
X = homer ;
false.
?- Y = xyz{}.father(X).
Y = fred,
X = james ;
false.
gender/3、parent/3 等的导出将随着即将发布的 Jekejeke Prolog 1.3.1 版本消失,当我们制作 ('.')/3 调用站点时知道的。但是 Jekejeke Prolog 的结果是一样的:
Jekejeke Prolog 3, Runtime Library 1.3.0
(c) 1985-2018, XLOG Technologies GmbH, Switzerland
?- Y = simpson{}.father(X).
Y = bart,
X = homer ;
Y = lisa,
X = homer ;
Y = maggie,
X = homer ;
No
?- Y = xyz{}.father(X).
Y = fred,
X = james ;
No
假设我们有下面的 familytree
模块(简单示例):
:- module(familytree, [
father/2,
mother/2,
%[...]
]).
father(X,Y) :- male(X),parent(X,Y).
father(unknown, _) :- male(unknown).
mother(X,Y) :- female(X),parent(X,Y).
mother(unknown, _) :- female(unknown).
sister(X,Y) :- female(X),parent(Z,X),parent(Z,Y), X \= Y.
%[... other relation predicates ... ]
我想将此模块谓词与不同的 "dbs" 一起使用,例如:
:- module(familytree_xyz, []).
male(james).
male(fred).
male(mike).
female(betty).
female(sandra).
parent(james, fred).
parent(betty, fred).
或:
:- module(familytree_simpson, []).
male(homer).
male(bart).
female(marge).
female(lisa).
parent(homer, bart).
%[...]
我需要:
- 在运行时而不是编译时选择数据库。
- 同时使用一个或多个数据库。
- 扩展数据库,例如。创建一个“familytree_simpson_extended”数据库模块,其他辛普森家族成员扩展“familytree_simpson”数据库模块(见上例)
- 符合 swi-prolog。
目前,我尝试使用 term_expansion/2
、discontiguous/1
、multifile/1
、dynamic/1
和 thread_local/1
指令,但是:
term_expansion/2
似乎只能在编译时使用,discontiguous/1
,multifile/1
,未改编,- 序言中的动态数据库被视为“邪恶”的做法,但是很多包和库使用它的(
pengines
,broadcast
模块,http
库,例如)。 thread_local/1
没有很好的文档记录,似乎不常用于 prolog 源代码 (swi-prolog)。
通过使用动态谓词,我将之前的代码更新如下:
%familytree.pl
:- module(familytree, [
familytree_cleanup_db/0,
familytree_use_db/1,
%[... previous declarations ...]
]).
dynamic male/1, female/1, parent/2.
familytree_cleanup_db :-
retractall(male/1),
retractall(female/1),
retractall(parent/2).
familytree_use_db(ModuleName) :-
assert(male(X) :- ModuleName:male(X)),
assert(female(X) :- ModuleName:female(X)),
assert(parent(X,Y) :- ModuleName:parent(X,Y)).
%[... previous predicates ...]
并且:
%main.pl
% use familytree tool predicates
:- use_module(familytree).
%load all familytree dbs at compile time.
:- use_module(familytree_xyz).
:- use_module(familytree_simpson).
:- use_module(familytree_simpson_extended).
main_xyz:-
familytree_cleanup_db,
familytree_use_db(familytree_xyz),
process.
main_simpson_all :-
familytree_cleanup_db,
familytree_use_db(familytree_simpson),
familytree_use_db(familytree_simpson_extended),
process.
process :-
findall(X, father(X,_), Xs),
write(Xs).
并且可以使用不同的数据库,如下所示:
?- main_simpson_all.
[homer,homer,abraham]
true.
?- main_xyz.
[james]
true.
所以,对于 post 的长度感到抱歉。问题:
使用此动态谓词解决方案要考虑的标准是什么,pros/cons?这是一个好的解决方案吗?
什么是最佳实践/特定的设计模式,以便序言在干净/健壮的代码中做到这一点?**
使用
thread_local/1
代替dynamic/1
并封装对新线程的调用以避免清理数据库有何意义?
由于源数据库显然在您的用例中起着重要作用,我建议在您的定义中明确其专用标识符,以便始终清楚您实际引用的是哪个家庭源:
db_male(xyz, james).
db_male(simpsons, bart).
db_female(xyz, betty).
db_female(simpsons, marge).
db_parent_of(xyz, james, fred).
因此,您基本上拥有 public 和多文件 db_male/2
、db_female/2
、db_parent_of/3
谓词。
自包含模块可以使用自己的源知识库扩展现有定义,在第一个参数中明确说明。这就是 term_expansion/2
等可能对您有所帮助的地方:由于数据库名称在每个单独的模块中都是相同的,因此您可以编写扩展代码来扩充 male/1
、female/1
的特定于模块的定义等使用合适的 db
参数并将其重写为 db_male/2
等。请注意,此重写只需要在 编译时 发生。在 运行 时,您可以提供您选择的任何数据库作为这些谓词的第一个参数。
female/1
、male/1
的全局定义也很明显:
male(M) :- db_male(_, M).
另请注意,我使用 parent_of/2
之类的名称来明确哪个参数是什么。
assertz/1
可用于在需要时动态扩充每个单独的数据库,再次明确提供名称。但是,为了干净和健壮的代码,我会在编译时尽可能多地做。
扩展我的评论,Logtalk 解决方案很简单。首先,用家庭关系谓词定义一个根对象:
:- object(familytree).
:- public([
father/2, mother/2,
sister/2, brother/2
]).
:- public([
parent/2,
male/1, female/1
]).
father(Father, Child) :-
::male(Father),
::parent(Father, Child).
mother(Mother, Child) :-
::female(Mother),
::parent(Mother, Child).
sister(Sister, Child) :-
::female(Sister),
::parent(Parent, Sister),
::parent(Parent, Child),
Sister \== Child.
brother(Brother, Child) :-
::male(Brother),
::parent(Parent, Brother),
::parent(Parent, Child),
Brother \== Child.
:- end_object.
请注意,male/1
、female/1
和 parent/2
定义的查找从 self 开始,即在对象中,数据库,它将接收有关家庭关系的查询。从您的示例代码派生的一个示例是:
:- object(simpsons,
extends(familytree)).
male(homer).
male(bart).
female(marge).
female(lisa).
parent(homer, bart).
parent(homer, lisa).
parent(marge, bart).
parent(marge, lisa).
:- end_object.
示例查询可以是:
?- simpsons::parent(homer, Child).
Child = bart ;
Child = lisa.
您可以根据需要添加任意数量的家族数据库,同时加载它们,并随意定义它们的专业化。例如:
:- object(simpsons_extended,
extends(simpsons)).
male(Male) :-
^^male(Male).
male(abe).
male(herb).
female(Male) :-
^^female(Male).
female(gaby).
female(mona).
parent(Parent, Child) :-
^^parent(Parent, Child).
parent(abe, homer).
parent(abe, herb).
parent(gaby, herb).
parent(mona, homer).
:- end_object.
这个解决方案可以满足您的所有要求。 SWI-Prolog 是受支持的 Prolog 编译器之一。您可以使用其安装程序安装 Logtalk。或者,对于 SWI-Prolog,您只需键入:
?- pack_install(logtalk).
更新
在您对此解决方案的评论中,您询问了有关将数据库注入家谱对象逻辑的问题。这很容易,但也需要不同的方法。首先定义familytree
为:
:- object(familytree).
:- public([
father/2, mother/2,
sister/2, brother/2
]).
:- public([
parent/2,
male/1, female/1
]).
:- multifile([
parent/2,
male/1, female/1
]).
father(Father, Child) :-
male(Father),
parent(Father, Child).
mother(Mother, Child) :-
female(Mother),
parent(Mother, Child).
sister(Sister, Child) :-
female(Sister),
parent(Parent, Sister),
parent(Parent, Child),
Sister \== Child.
brother(Brother, Child) :-
male(Brother),
parent(Parent, Brother),
parent(Parent, Child),
Brother \== Child.
:- end_object.
请注意,这是另一种选择,我们将 male/1
、female/1
和 parent/2
称为本地谓词,但它们也被声明为多文件谓词。现在我们需要在 familytree
对象中 "inject" 一个家庭数据库:
:- category(simpsons).
:- multifile([
familytree::male/1,
familytree::female/1,
familytree::parent/2
]).
familytree::male(homer).
familytree::male(bart).
familytree::female(marge).
familytree::female(lisa).
familytree::parent(homer, bart).
familytree::parent(homer, lisa).
familytree::parent(homer, maggie).
familytree::parent(marge, bart).
familytree::parent(marge, lisa).
familytree::parent(marge, maggie).
:- end_category.
用法示例(假设 familytree.lgt
和 simpsons.lgt
个文件):
?- {familytree, simpsons}.
...
yes
几个示例查询:
?- familytree::parent(homer, Child).
Child = bart ;
Child = lisa ;
Child = maggie.
?- familytree::male(Male).
Male = homer ;
Male = bart.
?- familytree::father(Father, Child).
Father = homer,
Child = bart ;
Father = homer,
Child = lisa ;
Father = homer,
Child = maggie ;
false.
另一种方法是使用 Prolog dicts。它们由 SWI-Prolog 引入,自 1.3.0 版以来,它们也可用于 Jekejeke Prolog。如果不需要接收器,可以简单地使用下划线。
文件simpson.pl:
:- module(simpson, [gender/3, parent/3]).
:- reexport(familytree).
_.gender(homer) := male.
_.gender(bart) := male.
_.gender(marge) := female.
_.gender(lisa) := female.
_.parent(homer) := bart.
_.parent(homer) := lisa.
_.parent(homer) := maggie.
_.parent(marge) := bart.
_.parent(marge) := lisa.
_.parent(marge) := maggie.
文件xyz.pl:
:- module(xyz, [gender/3, parent/3]).
:- reexport(familytree).
_.gender(james) := male.
_.gender(fred) := male.
_.gender(mike) := male.
_.gender(betty) := female.
_.gender(sandra) := female.
_.parent(james) := fred.
_.parent(betty) := fred.
文件familytree.pl:
:- module(familytree, [father/3]).
M.father(X) := Y :-
male = M.gender(X),
Y = M.parent(X).
要使家谱在 simpson 和 xyz 中也可见,请使用 reexport/1。这允许向 simpson 或 xyz 发送消息,但仍然会处理 familytree 中的方法。这是一个例子 运行:
Welcome to SWI-Prolog (threaded, 64 bits, version 7.7.19)
SWI-Prolog comes with ABSOLUTELY NO WARRANTY. This is free software.
?- Y = simpson{}.father(X).
Y = bart,
X = homer ;
Y = lisa,
X = homer ;
Y = maggie,
X = homer ;
false.
?- Y = xyz{}.father(X).
Y = fred,
X = james ;
false.
gender/3、parent/3 等的导出将随着即将发布的 Jekejeke Prolog 1.3.1 版本消失,当我们制作 ('.')/3 调用站点时知道的。但是 Jekejeke Prolog 的结果是一样的:
Jekejeke Prolog 3, Runtime Library 1.3.0
(c) 1985-2018, XLOG Technologies GmbH, Switzerland
?- Y = simpson{}.father(X).
Y = bart,
X = homer ;
Y = lisa,
X = homer ;
Y = maggie,
X = homer ;
No
?- Y = xyz{}.father(X).
Y = fred,
X = james ;
No