Hacker Timesnew | past | comments | ask | show | jobs | submitlogin

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.




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

Search: