One of the best kept secrets in Prolog programming, is the fact that the ISO module standard can be used for object oriented programming. To convey the idea how this is done, we will here compare the approach with Python. This is an easy 4 step tutorial with links to further information at the end.
The Pythonesk self-parameter is a convention to allow a procedure to act as an object method. Analogously a Prolog object receiver can be accessed as the first parameter of a Prolog predicate. Python has a convenience that the dot operator “.” is overloaded, and that it can now be used to invoke a method.
class Account: def deposit(self, amount): ... myaccount.deposit(1499)
In the Jekejeke Prolog runtime library the Pythonesk self-parameter approach has been adopted as well. Overloading dot operator “.” is not feasible for the Prolog programming language since it doesn’t have types. Therefore we decided to introduce a new explicit message sending operator (::)/2.
:- module(account, [deposit/2]). deposit(Self, Amount) :- ... MyAccount::deposit(1499)
The operator is similar to the module invocation operator (:)/2. The later operator can be used for an ordinary predicate call inside a module. The new operator uses some apparatus of the old operator but additionally places the object receiver as the first argument. In other Prolog systems like SWI-Prolog the operator can be bootstrapped as follows, but we provide it natively:
O::M :- functor(O, C, _), M =.. [F|L], G =.. [F,O|L], C:G.
The most surprising thing is the fact that the ISO module directive reexport/1 can be used for inheritance. But currently a Prolog system will differ from Python, in that overriding will be made explicit. This is unlike python where we can override without warning:
class Parent(): def show(self): ... class Child(Parent): def show(self): ... super().show() ...
The Jekejeke Prolog runtime library issues a warning when it sees some overriding. Unless the corresponding predicate clauses are preceded by the override directive. SWI-Prolog shows a similar warning, but doesn't have and override directive. In SWI-Prolog reexport/2 with an except/1 option would be needed:
:- module(parent, [show/1]). show(Self) :- ... :- module('child', [show/1]). :- reexport(parent). :- override show/1. show(Self) :- ... parent:show(Self) ...
A predicate might want to call the overridden predicate. Python provides the super() construct to allow calling an overridden method without late binding. The ISO module standard allows doing the same by explicitly mentioning the super module in a qualified call. The qualified call will require the Pythonesk convention in that it will have to mention the receiver object.
The classes are Prolog modules approach comes handy. Ordinary Prolog terms can play the role of immutable value objects and partake in Prolog object oriented programming based on the Pythonesk self-parameter convention. Python objects can be created by mentioning the class name and an objects lifecycle ends with a delete.
ref = myclass(arg1, arg2, ...) del(ref)
Prolog objects can be directly written down as ordinary Prolog terms. The ordinary Prolog terms has an associated module which is the functor of the atom or compound. It is not necessary to explicitly delete a Prolog object, it subject to the ordinary Prolog term lifecycle.
Ref = mymodule(arg1, arg2, ...)
Because classes are Prolog modules, the functor of a Prolog term defines its class and the arguments of a Prolog term define its state. The state can be accessed by ISO core standard predicates such as arg/3. Jekejeke Prolog provides a further predicate set_arg/4 which can be bootstrapped in SWI-Prolog as follows, but is provided natively in out system:
set_arg(N, C, A, D) :- C =.. [F|L], nth1(N, L, _, H), nth1(N, R, A, H), D =.. [F|R].
We demonstrate how ordinary Prolog terms might carry some state and serve as Prolog instances of Prolog classes. This is a convenient way to have a multitude of instances without the need to record the instance relationship. Further in principle Prolog terms do not require more than the ISO core standard for their construction and/or deconstruction:
The class beagle has a parent class dog. We place some common functionality for all dogs into the module dog. In particular we will provide a predicate name/2 that will access the name of a dog. The predicate name/2 is then used in the predicate bark/1, displaying the name of the dog when producing a message:
name(Self, Name) :- arg(1, Self, Name). bark(Self) :- Self::barking(Barking), Self::name(Name), write(Name), write(' says '), write(Barking), write('.'), nl.
There is the default implementation of barking/1 that returns "woof" as the dog voice. But the dog type "rottweiler" overrides barking/1 and returns "ruff". Here is what running the bark/1 method from within the top-level of our Prolog system will give:
?- example02/beagle(sally)::bark. sally says woof. ?- example02/beagle(coco)::bark. coco says woof.
The module "dog" also implements a convenience method set_name/3 which uses set_arg/4 under the hood. Since Prolog terms are immutable, it will yield a new Prolog term with a changed name. Here is a screenshot of invoking the set_name/3 and the bark/1 method from within the top-level of our Prolog system:
This completes our introduction to Prolog Object Orientation for Python Programmers. There is more to say how an integration with Java is possible but we spare this for another Qiita tutorial. The links at the end of this post point to the full Jekejeke Prolog runtime library tutorial and example code.
User Manual: Advanced Programming
Open Source: Advanced Examples