A category at the top

Logtalk supports both prototypes and classes/instances. While classes and instances provide a clear distinction between abstractions and materializations of those abstractions, prototypes are usually used to represent concrete, one of a kind entities. But prototypes can also be derived from similar prototypes by stating what makes them different from their parent prototypes. The elephants example (videocast) in the Logtalk distribution nicely illustrates this idea. We start by representing Clyde, a typical elephant, grey, and with four legs:

% clyde, our prototypical but also concrete elephant
:- object(clyde).

    :- public(color/1).
    color(grey).

    :- public(number_of_legs/1).
    number_of_legs(4).

:- end_object.

This is a fine representation if we have a single elephant or just a few elephants. But we can have a rare albino elephant like Fred. We could represent it as a standalone prototype like we did for Clyde. Or we can derive its representation from Clyde’s representation:

% fred, another elephant, is like clyde, except that he's white
:- object(fred,
    extends(clyde)).

    % override inherited definition
    color(white).

:- end_object.

At this point we may start thinking: should we use classes and instances instead? Instead of prototypes, we could have an elephant class with clyde and fred as instances. The class could define default values for color and number for legs that could be overriden in instances if necessary. Why use prototypes in the first place? In general, how do we decide between using prototypes or classes?

One clear advantage of prototypes, already mentioned, is that they simplify representing one of a kind entities. They are a trivial implementation of the singleton design pattern. With classes, we would need to define a class, a single instance, and possibly some solution for ensuring no other instances would be defined or created. But then Fred or some other unique elephant could come stomp on our carefully written code. With prototypes, there is no need to redo our class hierarchy. We just represent what’s different from an existing prototype as each prototype can have unique attributes. E.g. an elephant named Rose with a cute birth mark:

% rose, an elephant, is like clyde, but with a cute birth mark
:- object(rose,
    extends(clyde)).

    :- public(birth_mark/1).
    birth_mark(heart_shaped).

:- end_object.

With classes, we would need to introduce subclasses for each unique attribute or each combination of unique attributes. Here, categories could help avoid finding ourselves entangled with multiple inheritance and a combinatorial explosion of subclasses (see e.g. the points example). Still, classes work best when we can anticipate all our representation requirements. Prototypes work best when exceptions are common and unpredictable. Prototypes are also arguably simpler as there’s a single relation, extends, while with classes we have two relations, instantiates and specializes. But we can find ourselves in a situation where we prefer prototypes and feeling envy of class organizational features. For example, instead of having most new elephants derived from clyde, or an ad hoc solution with elephants derived from diverse elephants, we may want to define a prototypical but abstract elephant to declare and possibly define common attributes like color, number of legs, or birth date. Trouble is, nothing would prevent sending a message to that prototypical elephant even if nonsensical like birth date, which is only defined for concrete elephants. The solution: a category at the top that can be imported by any prototype (a category is a fine-grained, cohesive, set of predicate declarations and definitions that can be imported by any number of objects). We could, in some cases, use a protocol but a category allows us not only to declare predicates but also provide default definitions for the predicates. In our example, we could define:

:- category(elephant_commons).

    :- public(color/1).
    % default color
    color(grey).

    :- public(number_of_legs/1).
    % default number of legs
    number_of_legs(4).

    :- public(birth_date/1).
    % no default

:- end_category.

And then, redefine clyde, fred, and rose as follows:

:- object(clyde,
    imports(elephant_commons)).

:- end_object.


:- object(fred,
    imports(elephant_commons)).

    % override inherited definition
    color(white).

:- end_object.


:- object(rose,
    imports(elephant_commons)).

    :- public(birth_mark/1).
    birth_mark(heart_shaped).

:- end_object.

Given that we cannot send messages to categories, we don’t need to be careful to avoid a prototypical but abstract elephant when e.g. enumerating elephants and sending them messages. Thus, by taking advantage of categories, we get some of the organizational features of classes in a more lightweight and flexible solution that is more amenable to unforeseen representation requirements.