Imagine we have the familytree
module below (simple example) :
:- 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 ... ]
I want to use this module predicates with different "dbs", for examples with :
:- module(familytree_xyz, []).
male(james).
male(fred).
male(mike).
female(betty).
female(sandra).
parent(james, fred).
parent(betty, fred).
Or :
:- module(familytree_simpson, []).
male(homer).
male(bart).
female(marge).
female(lisa).
parent(homer, bart).
%[...]
I need :
- to choose db on runtime, not on compilation.
- to use one or more dbs in same time.
- to extend db, for eg. create a “familytree_simpson_extended” db module with other Simpson family members extending “familytree_simpson” db module (see above example)
- to be swi-prolog compliant.
For now, I tried to play with term_expansion/2
, discontiguous/1
, multifile/1
, dynamic/1
and thread_local/1
directives, but :
term_expansion/2
seems only usable on compile time,
discontiguous/1
, multifile/1
, not adapted,
- dynamic dbs in prolog are seen as an “Evil” practice, however lot of packages and libraries use its (
pengines
, broadcast
module,http
lib, for examples).
thread_local/1
is not very documented and seems not often used in prolog source code (swi-prolog).
With playing with dynamic predicate, I update previous code as follow :
%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 ...]
And :
%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).
And it's ok to use with different db as follow :
?- main_simpson_all.
[homer,homer,abraham]
true.
?- main_xyz.
[james]
true.
So, sorry for the length of the post. Questions :
What are the criteria, pros/cons to consider with this dynamic predicates solution ? is it a good solution ?
What are the best practice / specific design pattern for prolog to do that in a clean / robust code ?**
What's about using thread_local/1
instead dynamic/1
and encapsulate call to new thread to avoid cleanup db?
Expanding my comment, the Logtalk solution is straightforward. First, define a root object with the family relations predicate:
:- 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.
Note that the lookup of the definitions of the male/1
, female/1
, and parent/2
starts in self, i.e. in the object, the database, that will receive the queries about the family relations. An example, derived from your sample code would be:
:- 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.
An example query can be:
?- simpsons::parent(homer, Child).
Child = bart ;
Child = lisa.
You can them as many family databases as you want, load them at the same time, and define specializations of them at will. For example:
:- 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.
This solution fulfills all your requirements. SWI-Prolog is one of the supported Prolog compilers. You can install Logtalk using on of its installers. Alternatively, for SWI-Prolog, you can simply type:
?- pack_install(logtalk).
Update
In your comment to this solution, you asked about injecting a database into the family tree object logic. That's easy but it also requires a different approach. First define familytree
as:
:- 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.
Note that is this alternative, we call male/1
, female/1
, and parent/2
as local predicates but they are also declared as multifile predicates. Now we need to "inject" a family database in the familytree
object:
:- 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.
Usage example (assuming familytree.lgt
and simpsons.lgt
files):
?- {familytree, simpsons}.
...
yes
A couple of sample queries:
?- 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.
Since the source database obviously plays an important role in your use case, I suggest to make its dedicated identifier explicit in your definitions, so that it is always clear which family source you are actually referencing:
db_male(xyz, james).
db_male(simpsons, bart).
db_female(xyz, betty).
db_female(simpsons, marge).
db_parent_of(xyz, james, fred).
So, you basically have the public and multifile db_male/2
, db_female/2
, db_parent_of/3
predicates.
Self-contained modules can extend the existing definitions with their own source knowledge bases, made explicit in the first argument. This is where term_expansion/2
and the like may help you: Since the database name is the same within each single module, you can write expansion code that augments module-specific definitions of male/1
, female/1
etc. with the suitable db
argument and rewrites this to db_male/2
etc. Notice that this rewriting need only happen at compilation time. At run-time, you can supply any DB you choose as the first argument of these predicates.
It is also obvious how global definitions of female/1
, male/1
may look like:
male(M) :- db_male(_, M).
Notice also that I am using names like parent_of/2
to make clear which argument is what.
assertz/1
can be used to dynamically augment each individual database when required, again supplying the name explicitly. However, for clean and robust code, I would do as much as possible at compilation time.
An alternative is the use of Prolog dicts. They have been introduced by SWI-Prolog and since release 1.3.0 they are also available in Jekejeke Prolog. If the receiver is not needed, one can simply use an underscore.
File 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.
File 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.
File familytree.pl:
:- module(familytree, [father/3]).
M.father(X) := Y :-
male = M.gender(X),
Y = M.parent(X).
To make the familytree also visible in simpson and xyz use reexport/1. This allows sending a message to simpson or xyz, but nevertheless a method from familytree will be processed. Here is an example run:
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.
The exports of gender/3, parent/3, etc.. will go away with the upcoming release 1.3.1 of Jekejeke Prolog when we have made ('.')/3 call-site aware. But the result for Jekejeke Prolog is the same:
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