The "many worlds" design pattern

The many worlds design pattern is one of the most common patterns in Logtalk and Prolog applications. It allows reasoning about different worlds, where a world can be e.g. a dataset, a knowledge base, a set of examples. While this design pattern can be trivially implemented when only a single world is defined at any single time, a solution allowing multiple worlds to be defined concurrently is often desirable or required. Logtalk provides two sensible and simple implementation solutions, using inheritance and parametric objects.

To illustrate this design pattern and its possible implementations, consider the classic family relations example. In this case, we have basic facts about each family member like sex and parents. From these facts, we want to infer family relations such as father, mother, sister, or brother. As concrete family examples, we’re going to use the Addams family and the Simpson family. You can blame these choices on too much TV and movies growing up.

Inheritance based solution

In this solution, we have a root object defining the family relations with the concrete families defined as descendant objects. The implementation of the family relations simply call the basic family facts by sending messages to self:

:- object(family).

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

We can now define e.g. the Addams family as a descendant object:

:- object(addams,
    extends(family)).

    male(gomez).
    male(pubert).
    male(pugsley).

    female(morticia).
    female(wednesday).

    parent(gomez, pubert).
    parent(gomez, pugsley).
    parent(gomez, wednesday).
    parent(morticia, pubert).
    parent(morticia, pugsley).
    parent(morticia, wednesday).

:- end_object.

Querying a family relation translates to a message to the family object. For example:

| ?- addams::mother(Mother, Child).

Mother = morticia,
Child = pubert ;
Mother = morticia,
Child = pugsley ;
Mother = morticia,
Child = wednesday ;
no

An interesting variation of this implementation is to use a root category instead of a root object. In this alternative, the concrete family objects import the category instead of extending a root object. The advantage is that it’s no longer possible to send messages to the root entity itself that could never be answered as it represents an abstraction. A more classical solution is to define family as a class instead of a prototype (or category) and to define concrete families like addams as class instances.

Parametric object based solution

In this alternative solution we define the family relations in a parametric object that takes as parameter an object implementing the basic facts for a concrete family:

:- object(family(_Family_)).

    :- public([
        father/2, mother/2, sister/2, brother/2
    ]).

    :- uses(_Family_, [
        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.

In this case, the basic family facts are accessed by sending an (implicit) message to the object passed as a parameter. Here is becomes convenient to define a family protocol that any concrete family can implement:

:- protocol(family_basic_relations)

    :- public([
        parent/2, male/1, female/1
    ]).

:- end_protocol.

The Simpson family can be then represented by the following object:

:- object(simpson,
    implements(family_basic_relations)).

    male(homer).
    male(bart).

    female(lisa).
    female(maggie).
    female(marge).

    parent(homer, bart).
    parent(homer, lisa).
    parent(homer, maggie).
    parent(marge, bart).
    parent(marge, lisa).
    parent(marge, maggie).

:- end_object.

To query a concrete family, we send a message to the parametric object:

| ?- family(simpson)::father(Father, Child).

Father = homer,
Child = bart ;
Father = homer,
Child = lisa ;
Father = homer,
Child = maggie ;
no

Comparing the solutions

Scalability: The definition of multiple concrete worlds is only limited by the available memory in both solutions. The definiton of relations over the basic facts to describe concrete worlds require the same effort.

Extensibility: In the inheritance based solution, any new relation that we add to the family object becomes immediately available for use with the concrete family objects. Likewise, in the parametric object based solution, any new relation that we add to the family/1 parametric object becomes immediately available for querying concrete family objects. One advantage of the inheritance based solution is that we can extend the root object and then have concrete worlds derived from those extensions without changing the fact that queries are still sent to the concrete world objects. Doing the same with the parametric object based solution would require sending queries about concrete world objects to a different parametric object.

Performance: Both solutions use necessarily dynamic binding. In the case of the inheritance based solution, we use messages to self as the concrete family is only know at runtime. In the case of the parametric object based solution, we send messages to a concrete family object (passed as a parameter) that is also only known at runtime. For critical performance applications, a third solution is possible using multifile predicates to enable static binding. This multifile based solution requires, however, some boilerplate code and thus forgoes the simplicity of the two main solutions detailed above.

Resources

The Logtalk distribution includes a discussion of this design pattern in its design_patterns example, including sample implementations. It also includes family and family_alt examples with the latter illustrating the third alternative solution using multifile predicates mentioned above.