Objected Oriented Programming in SWI-Prolog

I read somewhere that you can think of modules as objects in Prolog. I am trying to get my head around this, and if it a good way to code.

If I have two files, one defining a class dog and then another one that uses this class to make two dog objects.

:- module(dog,
      [ create_dog/4,bark/1 ]).

create_dog(Name,Age,Type,Dog):-
   Dog = dog(name(Name),age(Age),type(Type)).

bark(Dog):-
   Dog = dog(name(_Name),age(_Age),type(Type)),
   Type = bassethound,
   woof.
bark(Dog):-
   Dog = dog(name(_Name),age(_Age),type(Type)),
   Type \= bassethound,
   ruff.

woof:-format("woof~n").

ruff:-format("ruff~n").

second file

use_module(library(dog)).

run:-
   dog:create_dog('fred',5,bassethound,Dog),
   forall(between(1,5,_X),
       dog:bark(Dog)
      ),
   dog:create_dog('fido',6,bloodhound,Dog2),
   dog:bark(Dog2).

This makes a dog object Dog which is a basset hound and makes it bark 5 times, I then make another dog object Dog2 which is a bloodhound and make this also bark. I understand that in oop you have objects that have behaviours and state. So I now have two objects with different behaviours based on their own states but at the moment I am storing the state of the objects in the Dog variables where they can be seen by the code in the main program. Is there a way to hide the state of the objects i.e to have private variables? For example I might want to have a way of storing the state has_barked for each dog object, which would be true if it has barked earlier in the program and false otherwise, then change the behaviour of bark/1 based on this.

Also how would you handle inheritance and overriding methods etc? Any pointer to readings welcomed. Thank you.


Solution 1:

Just an example of one of the possible reimplementations of your sample code in Logtalk. It uses prototypes for simplicity but it still illustrates some key concepts including inheritance, default predicate definitions, static and dynamic objects, and parametric objects.

% a generic dog
:- object(dog).

    :- public([
        create_dog/3, bark/0, name/1, age/1
    ]).

    create_dog(Name, Age, Dog) :-
        self(Type),
        create_object(Dog, [extends(Type)], [], [name(Name),age(Age)]).

    % default definition for all dogs
    bark :-
        write(ruff), nl.

:- end_object.


:- object(bassethound,
    extends(dog)).

    % bark different
    bark :-
        write(woof), nl.

:- end_object.


:- object(bloodhound,
    extends(dog)).

:- end_object.


% support representing dogs as plain database facts using a parametric object
:- object(dog(_Name,_Age,_Type),
    extends(dog)).

    name(Name) :-
        parameter(1, Name).

    age(Age) :-
        parameter(2, Age).

    bark :-
        parameter(3, Type),
        [Type::bark].

:- end_object.


% a couple of (static) dogs as parametric object proxies
dog(fred, 5, bassethound).
dog(fido, 6, bloodhound).


% another static object
:- object(frisbee,
    extends(bloodhound)).

    name(frisbee).
    age(1).

:- end_object.

Some sample queries:

$ swilgt
...
?- {dogs}.
% [ /Users/foo/dogs.lgt loaded ]
% (0 warnings)
true.

?- bassethound::bark.
woof
true.

?- bloodhound::bark.
ruff
true.

?- bassethound::create_dog(boss, 2, Dog).
Dog = o1.

?- o1::bark.
woof
true.

?- {dog(Name, Age, Type)}::bark.
woof
Name = fred,
Age = 5,
Type = bassethound ;
ruff
Name = fido,
Age = 6,
Type = bloodhound.

?- dog(ghost, 78, bloodhound)::(bark, age(Age)).
ruff
Age = 78.

?- forall(between(1,5,_X), {dog(fred,_,_)}::bark).
woof
woof
woof
woof
woof
true.

Some notes. ::/2 is the message sending control construct. The goal {Object}::Message simply proves Object using the plain Prolog database and then sends the message Message to the result. The goal [Object::Message] delegates a message to an object while keeping the original sender.

Solution 2:

Prolog modules can be trivially interpreted as objects (specifically, as prototypes). Prolog modules can be dynamically created, have a name that can be regarded as their identity (as it must be unique in a running session as the module namespace is flat), and can have dynamic state (using dynamic predicates local to the module). In most systems, however, they provide weak encapsulation in the sense that you can usually call any module predicate using explicit qualification (that said, at least one system, ECLiPSe, allows you to lock a module to prevent breaking encapsulation this way). There's also no support for separating interface from implementation or having multiple implementations of the same interface (you can somehow hack it, depending on the Prolog module system, but it's not pretty).

Logtalk, as mentioned in other answers, is a highly portable object-oriented extension to Prolog supporting most systems, including SWI-Prolog. Logtalk objects subsume Prolog modules, both from a conceptual and a practical point-of-view. The Logtalk compiler supports a common core of module features. You can use it e.g. to write module code in Prolog implementations without a module system. Logtalk can compile modules as objects and supports bi-directional calls between objects and modules.

Note that objects in Logic Programming are best seen as a code encapsulation and code reuse mechanism. Just like modules. OO concepts can be (and have been) successfully applied in other programming paradigms, including functional and logic. But that doesn't mean necessarily bringing along imperative/procedural concepts. As an example, the relations between an instance and its class or between a prototype as its parent can be interpreted as specifying a pattern of code reuse instead of being seen from a dynamic/state point-of-view (in fact, in OOP languages derived from imperative/procedural languages, an instance is little more than a glorified dynamic data structure whose specification is distributed between its class and its class superclasses).

Considering your sample code, you can recode it easily in Logtalk close to your formulation but also in other ways, the most interesting of them making use of no dynamic features. Storing state (as in dynamic state) is sometimes necessary and may even be the best solution for particular problems (Prolog have dynamic predicates for a reason!) but should be used with care and only when truly necessary. Using Logtalk doesn't change (nor intends to change) that.

I suggest you look into the extensive Logtalk documentation and its numerous programming examples. There you will find how to e.g. cleanly separate interface from implementation, how to use composition, inheritance, specialize or override inherited predicates, etc.

Solution 3:

Logtalk is effectively the prominent object oriented Prolog available today. Paulo made it available as a pack, so installing should be very easy.

Modules are not really appropriate for object orientation. They are more similar to namespaces, but without nesting. Also, the ISO standard it's a bit controversy.

SWI-Prolog v7 introduced dicts, an extension that at least handles an historical problem of the language, and make available 'fields' by name, and a syntax for 'methods'. But still, no inheritance...

edit

I've added here a small example of object orientation in SWI-Prolog. It's an evolution of my test application about creating genealogy trees.

Comparing the genealogy.pl sources, you can appreciate how the latest version uses the module specifier, instead of the directive :- multifile, and then can work with multiple trees.

You can see, the calling module is passed down the graph construction code, and have optional or mandatory predicates, that gets called by module qualification:

make_rank(M, RPs, Rp-P) :-
    findall(G, M:parent_child(P, G), Gs),
    maplist(generated(M, Rp, RPs), Gs).

optional predicates must be called like

...
catch(M:female(P),_,fail) -> C = red
...

Note that predicates are not exported by the applicative modules. Exporting them, AFAIK, breaks the object orientation.

==========

Another, maybe more trivial, example of of object orientation, it's the module pqGraphviz_emu, where I crafted a simple minded replacement of system level objects.

I explain: pqGraphviz it's a tiny layer - written in Qt - over Graphviz library. Graphviz - albeit in C - has an object oriented interface. Indeed, the API allows to create relevant objects (graphs, nodes, links) and then assign attributes to them. My layer attempts to keep the API most similar to the original. For instance, Graphviz creates a node with

Agnode_t* agnode(Agraph_t*,char*,int);

then I wrote with the C++ interface

PREDICATE(agnode, 4) {
    if (Agnode_t* N = agnode(graph(PL_A1), CP(PL_A2), PL_A3))
        return PL_A4 = N;
    return false;
}

We exchange pointers, and I have setup the Qt metatype facility to handle the typing... but since the interface is rather low level, I usually have a tiny middle layer that exposes a more applicative view, and it's this middle level interface that gets called from genealogy.pl:

make_node(G, Id, Np) :-
    make_node(G, Id, [], Np).
make_node(G, Id, As, Np) :-
    empty(node, N0),
    term_to_atom(Id, IdW),
    N = N0.put(id, IdW),
    alloc_new(N, Np),
    set_attrs(Np, As),
    dladd(G, nodes, Np).

In this snippet, you can see an example of the SWI-Prolog v7 dicts:

...
N = N0.put(id, IdW),
...

The memory allocation schema is handled in allocator.pl.

Solution 4:

Have a look at logtalk. It is kind of an object-oriented extension to Prolog.

http://logtalk.org/