Issues with Prolog

This page discusses some of the issues in conventional Prolog systems which have motivated Plang's design:

Cryptic syntax

Logic programming is a very powerful concept, able to express problems in artificial intelligence, theorem proving, and natural deduction very compactly. It is unfortunate that new users are often turned off by the cryptic nature of the traditional logic programming languages. The following are some examples (this list is not intended to be exhaustive):

A proficient user of these languages will no doubt see no problem with the above examples. However, the learning curve for a new user of the language can be quite steep. Key to Plang's design is the principle of use conventional syntax to perform conventional tasks.

For Plang, conventional means syntactic conventions that mainstream programmers are already familiar with. The following code is instantly recognizable:

if (X < Y)
    A;
else
    B;

The Prolog equivalent is not obvious to the uninitiated without consulting the language manual:

(X < Y) -> A ; B

The Prolog version is also error-prone. The following two statements are not equivalent:

if (X < Y) A;

(X < Y) -> A

The user's natural interpretation is that execution should continue with the next statement if the condition is false. In Prolog however, the -> operator will fail the entire clause if the condition is false and there is no "else" clause. It is necessary to do this instead to capture the user's actual intent:

(X < Y) -> A ; true

In Plang, commit can be used in place of !, because that is what the traditional Prolog "cut" does - commits the program to the current clause choice. For example, the following predicate is a safe version of list membership checking that does not loop infinitely if the list tail is a variable:

is_member(X, L) { var(L); commit; fail; }
is_member(X, [X|_]) { commit; }
is_member(X, [_|T]) { is_member(X, T); }

Syntactic sugar is not harmful!

The -> operator in the previous section is an example of syntactic sugar. Many Prolog systems will compile (A -> B ; C) by expanding it into normal Horn clause form first:

if_stmt_123 :- A, !, B.
if_stmt_123 :- C.

Oddly though, adding syntactic sugar for loop constructs is considered heretical. The following tail-recursive predicate iterates over all members of a list and performs an action:

perform_action([]).
perform_action([Head|Tail]) :- action(Head), perform_action(Tail).

Except this code is dangerous if the Tail is a variable. It may enter an infinite loop. A safer version is:

perform_action(List) :- var(List), !, fail.
perform_action([]).
perform_action([Head|Tail]) :- action(Head), perform_action(Tail).

In Plang, this can be expressed as follows:

perform_action(List)
{
    for (X in List)
        action(X);
}

This version is more obvious as to what it is doing, and closer to how a regular programmer thinks about solving the problem. The Plang parser internally expands the for loop to a hidden predicate similar in structure to perform_action above.

Another more involved example happens in programs that prompt the user for commands and then execute those commands. The programmer wants to write something like this:

main_loop()
{
    do {
        read_command(Cmd);
        switch (Cmd) {
        case 'save': save_to_file();
        case 'load': load_from_file();
        ...
        case 'quit': Quit = yes;
        default:     stdout::writeln('Invalid command');
        }
    } while (Quit !== yes);
}

Prolog instead forces the code to be unwound into multiple predicates:

main_loop :-
    read_command(Cmd),
    (Cmd !== quit ->
        execute_command(Cmd),
        main_loop
    ; true).

execute_command(save) :- !, save_to_file.
execute_command(load) :- !, load_from_file.
...
execute_command(X) :- !, write('Invalid command'), nl.

With multiple levels of commands, parameter error reporting, and so on, this can very quickly snowball into dozens of predicates micro-managing each tiny step. Plang's design recognizes that breaking code up into tiny steps is a job for a compiler, not a human!

Procedural code happens

One response to the previous examples is that they are procedural rather than declarative, which is against "The Prolog Way". However, code like the command processor above happens a lot in Prolog applications. Most applications have two main aspects:

The first aspect needs the full power of backtracking search to solve it. The second does not - it is implicitly procedural. It is possible (and encouraged!) to write declarative Horn clauses in Plang, but the language doesn't make it unnecessarily difficult to do other things as well.

Unwanted unification

It is very easy in Prolog to write a predicate that "over-unifies" its arguments, making it difficult to write certain kinds of matching algorithms. For example, the following predicate expands expressions in its first argument using the normal rules of arithmetic:

expand(A * (B + C), A * B + A * C).
expand((A + B) * C, A * C + B * C).

If we were to call expand((X + Y) * Z, Solution), then the predicate will respond with the following solutions:

(X + Y) * B + (X + Y) * C   where Z = B + C
X * Z + Y * Z

Clearly, only the second of these is what we intended with the predicate. The issue is that we want variables in the clause arguments (A, B, and C) to be bound, but not variables in the value we passed in (X, Y, and Z).

Traditional Prolog implementations often have a predicate that can freeze or lock a term to prevent unification of its variables. The drawback with that approach is that it requires the caller to know the internal details of the callee and then take steps to protect its terms from unexpected unification. This breaks encapsulation. Plang's solution is input-only arguments and one-way unification.


Generated on 26 May 2011 for plang by  doxygen 1.6.1