Share this page

Learn X in Y minutes

Where X=Logtalk

Logtalk is an object-oriented logic programming language that extends and leverages Prolog with modern code encapsulation and code reuse mechanisms without compromising its declarative programming features. Logtalk is implemented in highly portable code and can use most modern and standards compliant Prolog implementations as a back-end compiler.

To keep its size reasonable, this tutorial necessarily assumes that the reader have a working knowledge of Prolog and is biased towards describing Logtalk object-oriented features.

Syntax

Logtalk uses standard Prolog syntax with the addition of a few operators and directives for a smooth learning curve and wide portability. One important consequence is that Prolog code can be easily encapsulated in objects with little or no changes. Moreover, Logtalk can transparently interpret most Prolog modules as Logtalk objects.

The main operators are:

Some of the most important entity and predicate directives will be introduced in the next sections.

Entities and roles

Logtalk provides objects, protocols, and categories as first-class entities. Relations between entities define patterns of code reuse and the roles played by the entities. For example, when an object instantiates another object, the first object plays the role of an instance and the second object plays the role of a class. An extends relation between two objects implies that both objects play the role of prototypes, with one of them extending the other, its parent prototype.

Defining an object

An object encapsulates predicate declarations and definitions. Objects can be created dynamically but are usually static and defined in source files. A single source file can contain any number of entity definitions. A simple object, defining a list member public predicate:

:- object(list).

    :- public(member/2).
    member(Head, [Head| _]).
    member(Head, [_| Tail]) :-
        member(Head, Tail).

:- end_object.

Compiling and loading source files

Assuming that the code above for the list object is saved in a list.lgt file, it can be compiled and loaded using the logtalk_load/1 built-in predicate or its abbreviation, {}/1, with the file path as argument (the extension can be omitted):

?- {list}.
yes

In general, entities may have dependencies on entities defined in other source files (e.g. library entities). To load a file and all its dependencies, the advised solution is to define a loader file that loads all the necessary files for an application. A loader file is simply a source file, typically named loader.lgt, that makes calls to the logtalk_load/1-2 built-in predicates, usually from an initialization/1 directive for portability and standards compliance. Loader files are provided for all libraries, tools, and examples.

Sending a message to an object

The ::/2 infix operator is used to send a message to an object. As in Prolog, we can backtrack for alternative solutions:

?- list::member(X, [1,2,3]).
X = 1 ;
X = 2 ;
X = 3
yes

Encapsulation is enforced. A predicate can be declared public, protected, or private. It can also be local when there is no scope directive for it. For example:

:- object(scopes).

    :- private(bar/0).
    bar.

    local.

:- end_object.

Assuming the object is saved in a scopes.lgt file:

?- {scopes}.
yes

?- catch(scopes::bar, Error, true).
Error = error(
    permission_error(access, private_predicate, bar/0),
    logtalk(scopes::bar, user)
)
yes

?- catch(scopes::local, Error, true).
Error = error(
    existence_error(predicate_declaration, local/0),
    logtalk(scopes::local, user)
)
yes

When the predicate in a message is unknown for the object (the role it plays determines the lookup procedures), we also get an error. For example:

?- catch(scopes::unknown, Error, true).
Error = error(
    existence_error(predicate_declaration, unknown/0),
    logtalk(scopes::unknown, user)
)
yes

A subtle point is that predicate scope directives specify predicate calling semantics, not definition semantics. For example, if an object playing the role of a class declares a predicate private, the predicate can be defined in subclasses and instances but can only be called in its instances from the class.

Defining and implementing a protocol

Protocols contain predicate declarations that can be implemented by any number of objects and categories:

:- protocol(listp).

    :- public(member/2).

:- end_protocol.

:- object(list,
    implements(listp)).

    member(Head, [Head| _]).
    member(Head, [_| Tail]) :-
        member(Head, Tail).

:- end_object.

The scope of the protocol predicates can be restricted using protected or private implementation. For example:

:- object(stack,
    implements(private::listp)).

:- end_object.

In fact, all entity relations (in an entity opening directive) can be qualified as public (the default), protected, or private.

Prototypes

An object without an instantiation or specialization relation with another object plays the role of a prototype. A prototype can extend another object, its parent prototype.

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

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

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

:- end_object.

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

    color(white).

:- end_object.

When answering a message sent to an object playing the role of a prototype, we validate the message and look for an answer first in the prototype itself and, if not found, we delegate to the prototype parents if any:

?- fred::number_of_legs(N).
N = 4
yes

?- fred::color(C).
C = white
yes

A message is valid if the corresponding predicate is declared (and the sender is within scope) but it will fail, rather then throwing an error, if the predicate is not defined. This is called the closed-world assumption. For example, consider the following object, saved in a foo.lgt file:

:- object(foo).

    :- public(bar/0).

:- end_object.

Loading the file and trying to call the bar/0 predicate fails as expected. Note that this is different from calling an unknown predicate, which results in an error:

?- {foo}.
yes

?- foo::bar.
no

?- catch(foo::baz, Error, true).
Error = error(
    existence_error(predicate_declaration, baz/0),
    logtalk(foo::baz, user)
)
yes

Classes and instances

In order to define objects playing the role of classes and/or instances, an object must have at least an instantiation or a specialization relation with another object. Objects playing the role of meta-classes can be used when we need to see a class also as an instance. We use the following example to also illustrate how to dynamically create new objects at runtime:

% a simple, generic, metaclass defining a new/2 predicate for its instances
:- object(metaclass,
    instantiates(metaclass)).

    :- public(new/2).
    new(Instance, Clauses) :-
        self(Class),
        create_object(Instance, [instantiates(Class)], [], Clauses).

:- end_object.

% a simple class defining age/1 and name/1 predicate for its instances
:- object(person,
    instantiates(metaclass)).

    :- public([
        age/1, name/1
    ]).

    % a default value for age/1
    age(42).

:- end_object.

% a static instance of the class person
:- object(john,
    instantiates(person)).

    name(john).
    age(12).

:- end_object.

When answering a message sent to an object playing the role of an instance, we validate the message by starting in its class and going up to its class superclasses if necessary. Assuming that the message is valid, then we look for an answer starting in the instance itself:

?- person::new(Instance, [name(paulo)]).
Instance = o1
yes

?- o1::name(Name).
Name = paulo
yes

?- o1::age(Age).
Age = 42
yes

?- john::age(Age).
Age = 12
yes

Categories

A category is a fine grained unit of code reuse, used to encapsulate a cohesive set of predicate declarations and definitions, implementing a single functionality, that can be imported into any object. A category can thus be seen as the dual concept of a protocol. In the following example, we define categories representing car engines and then import them into car objects:

% a protocol describing engine characteristics
:- protocol(carenginep).

    :- public([
        reference/1,
        capacity/1,
        cylinders/1,
        horsepower_rpm/2,
        bore_stroke/2,
        fuel/1
    ]).

:- end_protocol.

% a typical engine defined as a category
:- category(classic,
    implements(carenginep)).

    reference('M180.940').
    capacity(2195).
    cylinders(6).
    horsepower_rpm(94, 4800).
    bore_stroke(80, 72.8).
    fuel(gasoline).

:- end_category.

% a souped up version of the previous engine
:- category(sport,
    extends(classic)).

    reference('M180.941').
    horsepower_rpm(HP, RPM) :-
        ^^horsepower_rpm(ClassicHP, ClassicRPM),    % "super" call
        HP is truncate(ClassicHP*1.23),
        RPM is truncate(ClassicRPM*0.762).

:- end_category.

% with engines (and other components), we may start "assembling" some cars
:- object(sedan,
    imports(classic)).

:- end_object.

:- object(coupe,
    imports(sport)).

:- end_object.

Categories are independently compiled and thus allow importing objects to be updated by simple updating the imported categories without requiring object recompilation. Categories also provide runtime transparency. I.e. the category protocol adds to the protocol of the objects importing the category:

?- sedan::current_predicate(Predicate).
Predicate = reference/1 ;
Predicate = capacity/1 ;
Predicate = cylinders/1 ;
Predicate = horsepower_rpm/2 ;
Predicate = bore_stroke/2 ;
Predicate = fuel/1
yes

Hot patching

Categories can be also be used for hot-patching objects. A category can add new predicates to an object and/or replace object predicate definitions. For example, consider the following object:

:- object(buggy).

    :- public(p/0).
    p :- write(foo).

:- end_object.

Assume that the object prints the wrong string when sent the message p/0:

?- {buggy}.
yes

?- buggy::p.
foo
yes

If the object source code is not available and we need to fix an application running the object code, we can simply define a category that fixes the buggy predicate:

:- category(patch,
    complements(buggy)).

    % fixed p/0 def
    p :- write(bar).

:- end_category.

After compiling and loading the category into the running application we will now get:

?- set_logtalk_flag(complements, allow).
yes

?- {patch}.
yes

?- buggy::p.
bar
yes

As hot-patching forcefully breaks encapsulation, the complements compiler flag can be set (globally or on a per-object basis) to allow, restrict, or prevent it.

Parametric objects and categories

Objects and categories can be parameterized by using as identifier a compound term instead of an atom. Object and category parameters are logical variables shared with all encapsulated predicates. An example with geometric circles:

:- object(circle(_Radius, _Color)).

    :- public([
        area/1, perimeter/1
    ]).

    area(Area) :-
        parameter(1, Radius),
        Area is pi*Radius*Radius.

    perimeter(Perimeter) :-
        parameter(1, Radius),
        Perimeter is 2*pi*Radius.

:- end_object.

Parametric objects are used just as any other object, usually providing values for the parameters when sending a message:

?- circle(1.23, blue)::area(Area).
Area = 4.75291
yes

Parametric objects also provide a simple way of associating a set of predicates with a plain Prolog predicate. Prolog facts can be interpreted as parametric object proxies when they have the same functor and arity as the identifiers of parametric objects. Handy syntax is provided to for working with proxies. For example, assuming the following clauses for a circle/2 predicate:

circle(1.23, blue).
circle(3.71, yellow).
circle(0.39, green).
circle(5.74, black).
circle(8.32, cyan).

With these clauses loaded, we can easily compute for example a list with the areas of all the circles:

?- findall(Area, {circle(_, _)}::area(Area), Areas).
Areas = [4.75291, 43.2412, 0.477836, 103.508, 217.468]
yes

The {Goal}::Message construct proves Goal, possibly instantiating any variables in it, and sends Message to the resulting term.

Events and monitors

Logtalk supports event-driven programming by allowing defining events and monitors for those events. An event is simply the sending of a message to an object. Interpreting message sending as an atomic activity, a before event and an after event are recognized. Event monitors define event handler predicates, before/3 and after/3, and can query, register, and delete a system-wide event registry that associates events with monitors. For example, a simple tracer for any message being sent using the ::/2 control construct can be defined as:

:- object(tracer,
    implements(monitoring)).    % built-in protocol for event handlers

    :- initialization(define_events(_, _, _, _, tracer)).

    before(Object, Message, Sender) :-
        write('call: '), writeq(Object), write(' <-- '), writeq(Message),
        write(' from '), writeq(Sender), nl.

    after(Object, Message, Sender) :-
        write('exit: '), writeq(Object), write(' <-- '), writeq(Message),
        write(' from '), writeq(Sender), nl.

:- end_object.

Assuming that the tracer object and the list object defined earlier are compiled and loaded, we can observe the event handlers in action by sending a message:

?- set_logtalk_flag(events, allow).
yes

?- list::member(X, [1,2,3]).

call: list <-- member(X, [1,2,3]) from user
exit: list <-- member(1, [1,2,3]) from user
X = 1 ;
exit: list <-- member(2, [1,2,3]) from user
X = 2 ;
exit: list <-- member(3, [1,2,3]) from user
X = 3
yes

Events can be set and deleted dynamically at runtime by calling the define_events/5 and abolish_events/5 built-in predicates.

Event-driven programming can be seen as a form of computational reflection. But note that events are only generated when using the ::/2 message-sending control construct.

Lambda expressions

Logtalk supports lambda expressions. Lambda parameters are represented using a list with the (>>)/2 infix operator connecting them to the lambda. Some simple examples using library meta:

?- {meta(loader)}.
yes

?- meta::map([X,Y]>>(Y is 2*X), [1,2,3], Ys).
Ys = [2,4,6]
yes

Currying is also supported:

?- meta::map([X]>>([Y]>>(Y is 2*X)), [1,2,3], Ys).
Ys = [2,4,6]
yes

Lambda free variables can be expressed using the extended syntax {Free1, ...}/[Parameter1, ...]>>Lambda.

Macros

Terms and goals in source files can be expanded at compile time by specifying a hook object that defines term-expansion and goal-expansion rules. For example, consider the following simple object, saved in a source.lgt file:

:- object(source).

    :- public(bar/1).
    bar(X) :- foo(X).

    foo(a). foo(b). foo(c).

:- end_object.

Assume the following hook object, saved in a my_macros.lgt file, that expands clauses and calls to the foo/1 local predicate:

:- object(my_macros,
    implements(expanding)).    % built-in protocol for expanding predicates

    term_expansion(foo(Char), baz(Code)) :-
        char_code(Char, Code). % standard built-in predicate

    goal_expansion(foo(X), baz(X)).

:- end_object.

After loading the macros file, we can then expand our source file with it using the hook compiler flag:

?- logtalk_load(my_macros), logtalk_load(source, [hook(my_macros)]).
yes

?- source::bar(X).
X = 97 ;
X = 98 ;
X = 99
true

The Logtalk library provides support for combining hook objects using different workflows (for example, defining a pipeline of expansions).

Further information

Visit the Logtalk website for more information.


Got a suggestion? A correction, perhaps? Open an Issue on the GitHub Repo, or make a pull request yourself!

Originally contributed by Paulo Moura, and updated by 1 contributor.