Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Coding Guidelines for Prolog (2011) (arxiv.org)
126 points by tosh on Sept 23, 2023 | hide | past | favorite | 14 comments


Very interesting! Let's consider for example the definition of sum_list/2 which is shown in Fig. 1, Fig. 2 and Fig. 3:

    %% sum_list(+Number_List, ?Result)
    % Unifies Result with the sum of the numbers in Number_List;
    % calls error/1 if Number_List is not a list of numbers.
    sum_list(Number_List, Result) :-
            sum_list(Number_List, 0, Result).
    
    % sum_list(+Number_List, +Accumulator, ?Result)
    sum_list([], A, A). % At end: unify with accumulator.
    sum_list([H|T], A, R) :- % Accumulate first and recur.
            number(H),
            !,
            B is A + H,
            sum_list(Rest, B, R).
    sum_list(_, _A, _R) :- % Catch ill-formed arguments.
            error('first arg to sum_list/2 not a list of numbers').
Compiling it with Scryer Prolog, I get a warning:

    $ scryer_prolog sum_list.pl
    Warning: singleton variables T, Rest at line 4 of sum_list.pl
       true.
A singleton variable often indicates a mistake in the code. And indeed, the sample code uses Rest where it apparently meant to use T (or vice versa). So, I change the second clause of sum_list/3 to:

    sum_list([H|T], A, R) :-
            number(H),
            !,
            B is A + H,
            sum_list(T, B, R).
And now we're ready to use the predicate! Let's ask Prolog the most general query: Which answers are there in general? So, a query where all arguments are logic variables:

    ?- sum_list(Ls, R).
       Ls = [], R = 0
    ;     error(existence_error(procedure,error/1),error/1).
The existence error is due to the use of the non-standard predicate error/1 in the code sample. The predicate apparently meant to throw an exception telling us:

    first arg to sum_list/2 not a list of numbers
But the query did not restrict the first argument at all, so it may just as well be a list of numbers! The predicate probably meant to say that the argument is not sufficiently instantiated. In that case, it should have thrown an instantiation error. The standard predicate (is)/2 throws such an instantiation error for us in such cases. Also, it throws type errors for us! A type error is categorically different from an instantiation error: From a logical perspective, a type error can be replaced by silent failure, but an instantiation error can not.

We can therefore write the second clause as:

    sum_list([H|T], A, R) :-
            B is A + H,
            sum_list(T, B, R).
and also remove the third clause entirely. We now get:

    ?- sum_list(Ls, R).
       Ls = [], R = 0
    ;     error(instantiation_error,(is)/2).
From a logical perspective, that's OK: The predicate tells us that too little is known to make any statement, and a more specific query may yield solutions.

Also, this version correctly distinguishes between type and instantiation errors, and we now get for example:

    ?- sum_list("abc", R).
       error(type_error(evaluable,a),(is)/2).
As I see it, a key attraction of logic programming is that we are able to reason logically about our code. This holds as long as certain logical properties are preserved. The paper hints at such properties for example with the concept of steadfastness, which it defines in Section 5.1: A predicate "must work correctly if its output variable already happens to be instantiated to the output value". How can we tell though which variables are output variables, and also why even distinguish a particular variable as "output variable"? Should this not hold for all variables?

A particularly important logical property is called monotonicity: Generalizing a query (or program) can at most add solutions, never remove them. With monotonic predicates, debugging is very nice: For instance, if a predicate unexpectedly fails, then we can generalize it by removing goals, and if the remaining fragment still fails unexpectedly, then there must be a mistake in that fragment. Scryer Prolog provides library(debug) for this approach of declarative debugging:

https://www.scryer.pl/debug

In Scryer Prolog, we can define the specific case of arithmetic over integers with constraints over integers, for example like this:

    :- use_module(library(clpz)).
    :- use_module(library(lists)).
    :- use_module(library(lambda)).
    
    list_sum(Is, S) :- foldl(\I^S0^S^(S #= S0 + I), Is, 0, S).
Higher-order predicates such as maplist/N and foldl/N retain logical properties of the predicates that occur as arguments.

The most general query now works as expected:

    ?- list_sum(Is, Sum).
       Is = [], Sum = 0
    ;  Is = [Sum], clpz:(Sum in inf..sup)
    ;  Is = [_A,_B], clpz:(_A+_B#=Sum)
    ;  Is = [_A,_B,_D], clpz:(_A+_B#=_C), clpz:(_C+_D#=Sum)
    ;  ... .
The predicate does not terminate, as expected, because we expect solutions for lists of all lengths:

    ?- list_sum(Is, Sum), false.
    loops.
And other cases are simply specific instances of the most general query:

    ?- list_sum([1,2,3], Sum).
       Sum = 6.
Note that I have changed the predicate name from sum_list/2 to list_sum/2, because the list is the first argument, and the sum is the second argument. So, I am now using "sum" no longer as a verb, but as a noun, because that seems more appropriate for code that is declarative, not imperative: We describe what is true, not what must be done, and our code works in all directions and also with different execution strategies. integers_sum/2 may be an even better name in this case.

One other naming convention I like to use is to append an "s" for logic variables that stand for lists, such as "Is" for a list of integers.


Great post, showing how these coding guidelines are showing their age. So much boilerplate comments, which clearly are not pulling their weight. A simple coding guideline like "eliminate all singleton variable warnings" would serve better, and it is enforced automatically to boot.

One thing we have learned is that if a coding guideline is mandatory, it just has to be automatically checkable and enforceable.


I'm not sure if this example is meant to be a great one -- they introduce it to illustrate different ways of typesetting.

The Craft of Prolog by one of the authors gave similar general advice to yours, as I remember it. (Good book.)

I've only glanced over this paper.


>does not terminate, as expected

okay but generally we want things to terminate. An infinite list of useless solutions is almost never what people want in an actual program.

which also ties into the definition of "the output variable", which is the information we wanted when we wrote the predicate.


> okay but generally we want things to terminate.

Not in this case. In this case the author claims that if there are infinitely many solutions, the predicate should give infinitely many solutions. It's similar to how you want functions in languages that use functions to fail predictably. Eg. you want to receive ENOENT exit code if you try to open a file that doesn't exist. Or, you want to block forever if you join a thread that runs an infinite loop.

Do you want to have any of those behaviors in your program? -- rarely, and sometimes not at all. But you want your program to fail in such a way that would indicate that you coded it in a particular way that should result in such an error.


I think he's just using it as a test here. He forces infinite backtracking by adding

  false
to the query. You wouldn't do that in an actual program.


I remember using this language in the AI class, i thought it was an old, deprecated language as people now use python mainly, really cool to see it again


I forget who said it, but a good programming language should have two features:

1. Make simple problems easy.

2. Make difficult problems possible.

Python is an outstanding language on both scores.

Prolog, alas, is much better at #2 than it is at #1. For example, prolog is a great choice for writing domain-specific languages, and modern implementations (like scryer prolog, mentioned in a comment above), can generate very efficient code for them.

Prolog's "killer ap" is how well it does on a third feature:

3. It makes virtually impossible tasks "merely" very difficult ;-)

And it has a steep learning curve; it took me years to really "get" it, and I only persevered because I was facing one of those category #3 problems.

Its not a deprecated language, but alas, it is destined to be a niche language. But for those niches, it is still the best, hands-down.


Can you tell us about the problem you've encountered in #3? I'm really interested


I guess this is rather category #2 than #3, but my favorite example is Advent of Code 2021, day 24: https://adventofcode.com/2021/day/24. I think many people consider this one of the hardest AOC problems ever and at the time many people didn't even code up a complete solution but solved in manually (me included). However, with Prolog it's almost trivial. So many of Prologs strengths come together here: writing parsers, writing interpreters, solving integer constraints, and in particular "reasoning backwards".


I was writing a theorem prover for higher-order modal logic. All the theorem provers written in C or C++ were tens of thousands of lines, and even at that they had only a fraction of what I needed. What's more, they were all like 100 times *slower* at proving theorems which prolog could prove out of the box.

So I decided to try to implement the features I needed on top of the theorem prover which prolog already is.

Took me forever, but eventually it all came together like a thunderclap, and I was able to implement a theorem prover for quantified, higher-order modal logic which was amazingly-blazingly fast--in 67 lines of prolog.

In terms of lines-of-code per day, its the least productive I've ever been :-)


While I wouldn’t want to do many tasks in prolog. I’ve always wished for a good interop with some other language, or a language with some kind of async prolog call. There are so many things that are a couple dozen of lines of prolog but would be hundreds of lines of other languages.


I think it is undergoing a resurgence due to the LLM prolifereating, but I haven't seen how Prolog is being used in the LLM creation. Of course, I don't really understand the whole process of the creation. I do remember someone on a academic/programmer forum stating Prolog regarding research was big in academics in Europe whereas Lisp was more popular in America academics for research. It would make sense as Richard Stallman was big into AI Lisp as a consultant.


I've been doing Prolog without knowing there's a coding guidelines. Thanks.




Consider applying for YC's Winter 2026 batch! Applications are open till Nov 10

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: