Object and predicate aliases

Naming is a non-trivial problem in programming. A good name conveys meaning, contributing to code readability. But there’s always a natural contention between descriptive names and concise names that help avoid excess verbosity. What’s a good name may also depend on its usage context. Moreover, if every library developer always use the best names, conflicts are bound to occur. The usual solution to these issues is to support renaming of object, module, and predicate names where they are used.

Object aliases

The uses/1 directive can be used to declare object aliases when the programmer prefers explicit message sending but also wants to use shorter names to minimize verbosity. For example, Logtalk provides a random library with multiple implementations of the same protocol, randomp. One of those implementations is named backend_random as it’s based on the backend Prolog compiler random number generator. It also provides two portable implementations, fast_random and random. We can use an object alias to both shorten the name and simplify experimenting with the different implementations:

:- object(foo).

    :- uses([
        backend_random as rnd
    ]).

    bar :-
        ...,
        % the same as backend_random::permutation(L, P)
        rnd::permutation(List, Permutation),
        ...

To experiment with a different implementation of the randomp protocol, we just need to change the rnd object alias definition and recompile.

Object aliases are specially useful when using parametric objects. For example, assume a library time/1 parametric object where the parameter is the time zone:

:- object(foo).

    :- uses([
        time('UTC−01:00') as time
    ]).

    bar :-
        ...,
        % the same as time('UTC−01:00')::now(Now)
        time::now(Now),
        ...

In this case, we bind the parameter in a single place in the code, ensuring consistency while simplifying usage and maintenance. This also works nicely for runtime bound parameters. For example, we can also write:

:- object(foo(_HeapOrder_, _OptionsObject_)).

    :- uses([
        heap(_HeapOrder_) as heap,
        _OptionsObject_ as options
    ]).

    bar :-
        ...,
        % the same as heap(_HeapOrder_)::as_heap(List, Heap)
        heap::as_heap(List, Heap),
        % the same as _OptionsObject_::get(ratio, Ratio)
        options::get(ratio, Ratio),
        ...

Predicate aliases

Predicate aliases provide similar benefits to object aliases but also allow solving name clashes. Unlike in current Prolog modules practice where a predicate name (e.g. member/2) is usually the exclusive of a specific module (e.g. lists) with other modules forced to use different names (e.g. random_member/2), library object predicates always use the best names.

The uses/2 directive can be used to declare predicate aliases for implicit message sending while also solving name conflicts. For example:

    :- uses(btrees, [new/1 as new_btree/1]).
    :- uses(queues, [new/1 as new_queue/1]).
    
    btree_to_queue :-
        ...,
        % the same as btrees::new(Tree)
        new_btree(Tree),
        % the same as queues::new(Queue)
        new_queue(Queue),
    ...

Name clashes can also occur when using multiple inheritance. For example, assume two objects, rpg_player and engineer, both declaring a rank/1 public predicate, and a third object, moms_basement_ghost, inheriting from the first two objects. In this case, we can use the alias/2 directive to provide access to both inherited predicates by giving them different aliases:

:- object(moms_basement_ghost,
    extends((rpg_player, engineer))).

    :- alias(rpg_player, [rank/1 as rpg_rank/1]).
    :- alias(engineer,   [rank/1 as engineer_rank/1]).

    rpg_rank(wizard).

    engineer_rank(senior).

:- end_object.

These aliases directives make possible the queries:

| ?- moms_basement_ghost::rpg_rank(Rank).

Rank = wizard
yes

| ?- moms_basement_ghost::engineer_rank(Rank).

Rank = senior
yes

As a side note, we can still use rank/1 as message. In this case, the default multiple inheritance conflict solver would use the definition, if any, inherited from the rpg_player object as it’s listed first in the moms_basement_ghost object opening directive.

Another common use of predicate aliases is to define an alternative predicate name simply to make it more clear in its usage context. For example, assume a sentences object exporting a transpose//0 non-terminal:

    % define an alternative name for a non-terminal:
    :- alias(sentences, [transpose//0 as flip//0]).

Logtalk 3.34.0 added support for defining predicate shorthands where one or more arguments can be omitted by binding the arguments in the original predicate call template. For example:

    :- uses(logtalk, [
        print_message(debug, my_app, Message) as dbg(Message)
    ]).

    bar :-
        ...,
        % same as logtalk::print_message(debug, my_app, @oh_no)
        dbg(@oh_no),
        ...

Logtalk version of the use_module/2 directive also provides similar support for defining predicate aliases and shorthands. For example:

:- object(foo(_OptionsModule_)).

    :- use_module(_OptionsModule_, [
        set/2, get/2, reset/0
    ])

    :- use_module(pairs, [
        map_list_to_pairs(length, Lists, Pairs) as length_pairs(Lists, Pairs)
    ]).

    ...

Final notes

Object and predicate aliases and shorthands can improve code readability, consistency, and maintenance. They also make experimenting with alternatives implementations easier by providing a single point of change. But they should also be used with some care as any reading and understanding of source code, specially long object definitions, must always take into account any defined aliases and shorthands.