Skip to content

Why Logtalk?

Paulo Moura edited this page Aug 23, 2018 · 70 revisions

Logtalk is designed to extend Prolog, not to replace it. It provides, however, an alternative for Prolog modules, subsuming their functionality. By Prolog modules we assume here the de facto standard module system introduced by Quintus Prolog and adapted by most of the Prolog systems that provide an implementation of modules. Although Prolog systems adapted the original module system introducing several proprietary variations (and consequent severe portability issues), the fundamental characteristics remain:

  • Designed as a simple solution to to hide auxiliary predicates
  • Based on a predicate prefixing mechanism
  • Reuse based on import/export semantics

These fundamental characteristics make module systems relatively simple to implement but also effectively prevent extending them to provide several key features present in Logtalk and described here.

Logtalk also fixes some murky semantics found in Prolog and improves some it its key mechanisms. Note that all advantages described next coexist with Prolog. I.e. Logtalk acts strictly as an add-on. It does not patch or modify Prolog own built-in features.

Separation between interface and implementation

Protocols are first-class entities in Logtalk. Being able to define a protocol (or interface) and provide multiple implementations is one of the most basic features for an encapsulation mechanisms. Protocols are also a key part of many design patterns in software engineering. Its absence in module systems twists practice and leads to non scalable, brittle solutions.

The current and recommended practice with Prolog modules is that exported predicates should not clash and to prefer use_module/1 directives over use_module/2 directives or explicit qualification. These recommendations are as convenient (specially for new users) as they are problematic. The first consequence, is that modules, as encapsulation units, are in practice only there to prevent clashes in private predicates. But there isn't any central authority for modules. Nor is reasonable to expect or demand that programmers all over the world sync before deciding the names of exported predicates when releasing a public module library. Users may also find that newly released libraries clash with their private modules. The preference for use_module/1 directives also means that adding a new exported predicate to a module library can cause a module conflict and thus break existing applications that, necessarily, don't use the new predicate. Another consequence is that module exported predicates are often prefixed with an abbreviation of the module name to prevent clashes (see e.g. the ordsets, random, or rbtrees modules).

Note that separation of interface and implementation for modules (with multiple modules implementing the same interface) is not a question of finding an hypothetical ingenious solution: is simply not possible without giving up module predicate import/export semantics as you cannot load in the same context two modules exporting the same predicates.

Same semantics for implicit and explicit qualified calls

In Logtalk, implicit message sending, using uses/2 directive to resolve the messages, and explicit message sending, using the ::/2 control construct, have the same semantics for both meta-predicates and non meta-predicates. I.e. using using implicit or explicit messages is simply a matter of programming style and programmer preference. For meta-predicates, this results in clear and clean meta-predicate semantics: meta-arguments are always called in the meta-predicate calling context.

Prolog modules, however, provide different semantics for implicit and explicit qualified calls to meta-predicates. Calling a module meta-predicate with implicit qualification (by using use_module/1-2 directives) results in the meta-arguments being called in the meta-predicate calling context. But calling the same module meta-predicate using explicit qualification results in the meta-arguments being called in the meta-predicate definition context, not in the calling context.

Clear distinction between declaring and defining a predicate

Logtalk provides clear closed world assumption semantics. Messages or calls for declared but undefined predicates fail. Messages or calls for unknown (not declared) predicates throw an error. Note that this is a fundamental requirement for protocols/interfaces: we must be able to declare a predicate without necessarily defining it.

In contrast, in module systems it's either a compiler error to try to export a predicate that is not defined or a predicate existence error to try to call an exported predicate that is not defined. The absence of a protocol/interface concept in module systems is sometimes hacked using include/1 directives. But that leads to versioning of files instead of versioning of interfaces and does not provide a solution that allows two or more modules to implement the same interface as discussed above.

Note that modules are a predicate prefixing solution and thus any call to a module predicate requires it to be defined in the module that is implicit or explicit in the call (otherwise a predicate existence error is generated). There is no predicate lookup that would allow to verify that a predicate is declared prior to calling it.

Clear separation between loading a file and using an entity

In Logtalk, loading a file does not imply any kind of using relation between existing entities and the entities being loaded. But in Prolog module systems, the use_module/1-2 directives take as first argument a specification of a file to be loaded and not a module name (which may or may not be the same as the file basename) and both load the file and import the listed predicates (in case of a use_module/2 directive) or all exported predicates of the module being loaded (in case of a use_module/1 directive). The ensure_loaded/1 directive also takes a file specification as argument but, depending on the Prolog system, it either behaves as use_module/1 directive or as a use_module/2 directive with an empty list as second argument. The potential problems of the implicit importing of the use_module/1 directive have already been discussed.

No predicate conflicts on loading

Loading two or more objects that declare the same predicate as public does not create any conflicts as Logtalk does not use the concept of predicates importing/exporting found in Prolog modules. This means e.g. that a Logtalk update that adds new public predicates to its libraries cannot break existing applications. But loading two modules exporting the same predicate is a compilation error that can only be solved by either renaming one of the predicates (in module systems that happen to support such feature) or by using a use_module/2 directive with an empty import list as second argument and resorting to explicit qualification. This means that any update that adds new exported predicates to module libraries have the potential to break existing applications.

Predicate names are not owned by objects

The same predicate name can be a public predicate of any number of objects. This means that object interfaces can use the best possible predicate names, simplifying and minimizing protocols vocabulary, making APIs easy to learn. In contrast, current practice with Prolog modules is that exported predicates should never conflict. But this is not scalable and results with different vocabulary being used for otherwise similar module interfaces. As an example, member/2 is a predicate usually exported by a lists module. But member/2 is also a good name for e.g. modules representing sets or trees. But the emphasis in implicit qualification using use_module/1 directives effectively prevents using the member/2 predicate name for anything other than lists.

Simple scope rules for all Logtalk flags

The set_logtalk_flag directive is always local to their scope, which can be an entity or a source file. Only calls to the set_logtalk_flag built-in predicate set the global default value for a flag.

In contrast, some Prolog flags have local scope while others have global scope. Worse, there are differences between Prolog systems that support modules with some flags being local to a module in a system and global in another. A notable case are op/3 directives, which are local to modules in some systems and global in others.

Enforcement of encapsulation

Most Prolog module systems allow any predicate to be called using explicit qualification. This is usually not an issue when coding guidelines are enforced by strong team discipline. Logtalk, instead of relying solely in good practices, enforces encapsulation and generates an error on any attempt to call a predicate that is out of scope. Logtalk also prevents using specially crafted meta-predicates to break encapsulation.

Strict compiler

Most Prolog compilers are permissive, silently accepting problematic code. The Logtalk compiler is strict and either rejects or warns the user of a large number of code issues, contributing to earlier detection and warning of source code issues. Lint checks include:

  • Missing directives (including scope, meta-predicate, dynamic, discontiguous, and multifile directives)
  • Duplicated directives
  • Missing predicates (calls to non-declared and non-defined predicates)
  • Calls to declared but not defined static predicates
  • Non-portable predicate calls, arithmetic function calls, directives, flags, and flag values
  • Suspicious calls (syntactically valid calls that are likely semantic errors)
  • Deprecated directives, control constructs, and flags
  • References to unknown entities (objects, protocols, categories, or modules)
  • Goals that are always true or always false
  • Trivial goal fails (due to no matching predicate clause)
  • Redefined built-in predicates
  • Lambda expression unclassified variables and mixed up variables
  • Singleton variables

Additional checks are provided by the make and dead_code_scanner tools.

Logtalk objects subsume Prolog modules

Logtalk objects subsume Prolog modules but the reverse is not true in general. You can't go from an object solution to a module solution easily or without significant hacking when you're taking advantage of e.g. inheritance, self/super calls, protocols, or parametric objects. Using a more general solution is worthy by itself and orthogonal to the programming in the small/large perspective.

As Prolog modules are objects, prototypes to be exact, most module-based solution can be easily translated into an object-based solution. So easy in fact that the Logtalk compiler does it for you (minus proprietary stuff that choke it, which varies from Prolog system to Prolog system; blame lack of standardization).

:- module(foo, [bar/1, baz/2]).
...

or

:- module(foo, []).
:- export([bar/1, baz/2]).
...

is simply interpreted by the Logtalk compiler as:

:- object(foo).
    :- public([bar/1, baz/2]).
    ...
:- end_object.

The Logtalk code is as readable as the module code, provides the same performance, and requires just one more directive, end_object/0, that any decent editor will type for you by expanding an object template. Note, however, that Logtalk does not use a predicate prefixing mechanism and does not provide the predicate exporting semantics found in Prolog modules. Thus, the interpretation of modules as objects is not a case of alternative syntax but results in different predicate usage semantics: by compiling a Prolog module as a Logtalk object, the exported predicates become public predicates that must be called either using the ::/2 message sending control construct or using an uses/2 directive to list them as implicit messages to the object.

Portability

Logtalk is written in highly portable code and currently supports 12 backend Prolog systems. It can support any Prolog system that complies with official and de facto standards. Logtalk libraries and developer tools are also portable.

In contrast, the ISO Prolog standard for modules is ignored (for sound reasons) by Prolog implementers. Worse, Prolog systems implementing a module system have significant differences that hinder portability. A few examples:

  • ECLiPSe - no module/2 directive
  • SICStus Prolog and XSB - no reexport/1-2 directives
  • ISO standard - no use_module/1-2 directives
  • SWI-Prolog - operators are local to modules
  • SICStus Prolog - operators are global
  • XSB - atom-based module system
  • ECLiPSe, SICStus Prolog, SWI-Prolog, YAP - predicate-based module system

Key mechanisms

Logtalk provides (fully portable) implementations of key mechanisms to all supported backend Prolog compilers. These include:

  • Conditional compilation directives
  • Term-expansion
  • Message printing
  • Question asking

Conditional compilation directives are found nowadays in most Prolog systems although some implementations are buggy, including in some high profile systems, if you need more than the very basic usage.

The term-expansion mechanism can be traced back to Quintus Prolog. Nevertheless, it is only found in some of the supported systems but with proprietary variations that hinder portability. The Logtalk implementation, besides being portable and working the same in all supported systems, provides improved functionality that subsumes most existing implementations and allows fine control of expansion pipelines.

The message printing mechanism can also be traced back to Quintus Prolog. But it is only supported by some Prolog systems. The Logtalk implementation extends the original implementation by introducing a component argument that allows easy filtering of messages per component in large applications and helps avoiding message term conflicts between components. It also provides support for meta messages avoiding the need of defining grammar rules for translating each and every message as in the original implementation.

The question asking mechanism is the dual of the message printing mechanism. It allows abstracting asking questions to a user in the same way that the message printing mechanism abstracts printing messages to the user. It is an uncommon mechanism. But it facilitates abstracting applications input/output interfaces while also allowing common requested features such as logging to be plugged in.

Clone this wiki locally