Search code examples
objectdesign-patternsmoduleprolog

Prolog Design Pattern to extend module predicates


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 :

  1. What are the criteria, pros/cons to consider with this dynamic predicates solution ? is it a good solution ?

  2. What are the best practice / specific design pattern for prolog to do that in a clean / robust code ?**

  3. What's about using thread_local/1 instead dynamic/1 and encapsulate call to new thread to avoid cleanup db?


Solution

  • 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.