3

I am trying to implement some simple predicates, something like my_length or my_append.

It is considered easy for me if we knew beforehand that we wanted to find the length of a list, or we wanted to append two lists. (i.e. I know what is input, what is output).

In Prolog, it is possible to do thing in other ways. Like my_length(L, 3), or my_append(A,B,[1,2,3]).

Sometimes, my code works. Sometimes, it doesn't.

I find it quite difficult to make sure it works in all sort of different ways. Unless it is just a helper predicate for myself, you never really know what your users want to test it with. Sometimes the problem can even be ill defined, it is unclear what should my_length(L, 5) outputs, for example.

Are there any best practices for that?

For practical programming, I found it much easier to just ignore these other ways and focus on only a particular way of calling. That's how I get things done, I am just worried about the possibility that someone else call it in a different way.

Is there a way for me to make that restriction at the language level? Or should I?

In particular, I am trying to write my_length such that it works for

  1. Specifying list, calculating length, and
  2. Specifying length, give me back a list that has length unbounded slot.
my_length([], 0).
my_length([_|T], A) :- my_length(T, TA), A is TA + 1.

That works fine both ways, except it prompts for more answers when I ask the reverse question, and then we get a stack overflow. We also can't do arithmetic with the length argument since it could be unspecified.

This is just a specific case.

  • As an example, Scryer's `length` is defined at https://github.com/mthom/scryer-prolog/blob/master/src/lib/lists.pl#L64 – brebs Feb 19 '23 at 20:28
  • Thanks, that's quite a lot of code to get it to work. – Cecilia Chen Feb 19 '23 at 22:24
  • The *best practices* part of this question is rather open-ended, and I think more suitable for https://www.reddit.com/r/prolog/ – brebs Feb 20 '23 at 22:21
  • 1
    @brebs: The biggest disadvantage of Reddit is its lack of an edit history. This makes it much too chatty, since people do not put the same kind of diligence into their answers. – false Feb 21 '23 at 13:06
  • Yes, but that can be better than people feeling obligated to write an essay, and therefore not bothering to contribute at all. – brebs Feb 21 '23 at 14:26
  • Don't re-implement built-ins (like length/2) or library predicates (like append/3). There is nothing to gain from reimplementing existing functionality badly. Open the source and understand how to _really_ do it (yes, quite a lot of code, what a shock!) or accept for now that those are given and write something _using those_. – TA_intern Feb 22 '23 at 11:18
  • "I am just worried about the possibility that someone else call it in a different way" if you only want to guard against unexpected uses, you can use `var/1` or `ground/1` etc. to detect them. – Will Ness Feb 27 '23 at 01:27

3 Answers3

3

Before answering your question, let's clarify what 'not working' may mean. It may mean that you may get an answer, but that answer is incorrect, like N = 3 when N = 2 would be the correct answer. Or it may mean that you get no answer, like a loop, instead. In traditional programming languages both are seen as undesirable and there is no clear distinction between those. In both cases such a program would be considered incorrect. But Prolog is a bit different here. The reason is that we have real variables at runtime. So we may have a list with two identical elements, say [X,X] which describes all possible lists with two identical elements. That is, we are describing here infinitely many such lists. Sometimes, these infinities can be described compactly, and sometimes this is not possible.

So infinity and its less popular companion non-termination are lurking around in Prolog right from the very first little programs on. We have to deal with it up front. Think of

?- append(L,[a],L).
   loops.

Looping behavior may be observed by getting a resource error, or just by a never ending query. In this particular case, all current Prolog systems will produce a resource error which causes some of them to abort completely.

What could be worse? Plenty! Think of

?- append(L,[a],L).
   L = [], unexpected.

Mastery of non-termination is absolutely essential in Prolog. In your case, the

my_length([], 0) :- false.
my_length([_|T], A) :- my_length(T, TA), false, A is TA + 1.

pinpoints you to the very problem. The second argument has no influence on termination whatsoever! That is, all queries with a known length will terminate as good or bad as the same queries with a variable for the length. And thus only the first argument influences termination. Thus only lists of known length (and terms like [a,b|non_list]) will terminate. We need to modify something in the visible part of the program. Otherwise the problem will persist.

Now to fix the problem, we have several options

Stay pure

In many situations, the best is to stick to the pure, monotonic subset of Prolog and thus simply add a corresponding goal in front.

In SICStus and Scryer say2:

:- use_module(library(clpz)).

my_length2([], 0).
my_length2([_|T], A) :- #A #>0, #A #= #TA+1, my_length2(T, TA).

?- asserta(clpz:monotonic).

So in this new version, the second argument now influences termination just as the first one. (There are more efficient ways to write this even in the pure, monotonic subset.)

Another way to improve termination may be coroutining, like freeze/2 or when/2. In this particular case this would be insufficient. But sometimes it can do wonders. The downside is that you may now get answers that are not solutions.

Again another way is to rule out certain problematic cases by means of instantiation errors. Prefer list_si/1 and iwhen/2 over manual tests.

Handle instantiations explicitly

The other way is extremely difficult, as it requires to handle all different kinds of instantiations separately. Avoid this, unless you want to delve into systems' programming. Many programmers think they can handle this, only to leave out a case or two. You have seen the efficient code for Scryer. Let's leave it at looking at it.

Conclusion

So why are these things so complex in Prolog? Well, these problems exist equally in traditional programming languages but due to their less expressive formalism, you need to write much more code (like implementing a Prolog interpreter) to encounter the same problems. Often (in particular in the context of Hoare logic) one distinguishes between partial and total correctness. And the problem behind is even more profound, since there are problems that are only semi-decidable. That is, where non-termination (or any other form of non-answer) is the only possible answer.


2 For SWI say instead

?- use_module(library(clpfd)).
?- op(150, fx, #).             % SWI misses this operator
?- asserta(clpfd:monotonic).

false
  • 10,264
  • 13
  • 101
  • 209
  • Playing Devil's advocate... clpz might as well be *magic*, to beginners. Is there no definition of `length` that is both friendly to non-experts and works as intended? Are such generators, that can spiral off into infinity, perhaps actually *bad* predicates? – brebs Feb 21 '23 at 19:45
  • `clpz` has many desirable properties that make it easier to understand for beginners than even the `clpfd` of SICStus (which is nevertheless also quite consistent compared to other implementations). In particular, `clpz`-related unifications all terminate whereas they may exhaust the finite integer range in SICStus. So failure slices are more uniform. What is even better is that clpz (optionally) ensures full monotonicity. – false Feb 22 '23 at 04:57
  • As for `length/2`, there are quite a lot of use cases for queries like `?- length(L, N).` even on SO. Think of fairly enumerating all sentences of a non-terminal. Or finding counterexamples. WG17 [settled](http://www.complang.tuwien.ac.at/ulrich/iso-prolog/length) for specific behaviors based on actual use and general consistency via template and modes. – false Feb 22 '23 at 05:05
  • *exhaust the finite integer range in SICStus* to this I should add that in this case SICStus very conformingly produces (after exhausting the domain) a representation error which is a far better response than (incorrect) silent failure. – false Feb 24 '23 at 08:21
0

Without the dangers which come with being a generator going off to infinity:

list_length_freeze(Lst, Len) :-
    freeze(Len, (integer(Len), Len @>= 0)),
    list_length_freeze_(Lst, 0, Len).
    
list_length_freeze_(Lst, Upto, Len) :-
    % Checks for determinism
    (   Lst == [] -> Upto = Len
    ;   Upto == Len -> Lst = []
    % Base case
    ;   Lst = [], Upto = Len
    ;   Lst = [_|T],
        inc_int_below_max(Upto, Len, Upto1),
        when(
            (nonvar(T) ; nonvar(Len)),
            list_length_freeze_(T, Upto1, Len)
        )
    ).

% Increment, keeping to maximum (if specified)
inc_int_below_max(I, Max, I1) :-
    I1 is I + 1,
    % Check against Max if specified
    (integer(Max) -> I1 @=< Max ; var(Max)).

Results in swi-prolog:

?- list_length_freeze(Lst, Len).
Lst = [],
Len = 0 ;
Lst = [_|_A],
freeze(Len, (integer(Len), Len@>=0)),
when((nonvar(_A);nonvar(Len)), list_length_freeze_(_A, 1, Len)).

?- list_length_freeze(Lst, Len), (Lst = [a] ; Lst = [a,b]).
Lst = [a],
Len = 1 ;
Lst = [a, b],
Len = 2.

?- list_length_freeze(Lst, Len), Len = 3.
Lst = [_, _, _],
Len = 3.

?- list_length_freeze(L, L).
false.

?- freeze(L, (K = 1 ; K = 2)), list_length_max_freeze(L, 0).
L = [],
K = 1 ;
L = [],
K = 2.

?- list_length_freeze(L, N), L = [a|N], N = 1.
false.

That last example is an improvement over swi-prolog's length:

?- length(L, N), L = [a|N].
ERROR: Stack limit (1.0Gb) exceeded

Another way to prevent going off to infinity, is to use coroutining to set a maximum list length:

list_length_max_freeze(Lst, Max) :-
    freeze(Max, (integer(Max), Max @>= 0)),
    list_length_max_freeze_(Lst, Max, 0).

list_length_max_freeze_(L, Max, Upto) :-
    nonvar(Max),
    !,
    (   Max @> Upto
    ->  (   var(L)
        ->  freeze(L, list_length_max_freeze_(L, Max, Upto))
        ;   (   L = []
            ->  true
            ;   L = [_|T],
                Upto1 is Upto + 1,
                list_length_max_freeze_(T, Max, Upto1)
            )
        )
    ;   Max == Upto
    ->  L = []
    ).
list_length_max_freeze_(L, Max, Upto) :-
    (   var(L)
    ->  when((nonvar(L) ; nonvar(Max)),
            list_length_max_freeze_(L, Max, Upto)
        )
    ;   (   L = []
        ->  freeze(Max, Max @>= Upto)
        ;   L = [_|T],
            Upto1 is Upto + 1,
            when((nonvar(T) ; nonvar(Max)),
                list_length_max_freeze_(T, Max, Upto1)
            )
        )
    ).

Results in swi-prolog:

?- list_length_max_freeze(Lst, 3).
freeze(Lst, list_length_max_freeze_(Lst, 3, 0)).

?- list_length_max_freeze(Lst, 3), length(Lst, 2).
Lst = [_, _].

?- list_length_max_freeze(Lst, 3), length(Lst, Len).
Lst = [],
Len = 0 ;
Lst = [_],
Len = 1 ;
Lst = [_, _],
Len = 2 ;
Lst = [_, _, _],
Len = 3.
brebs
  • 3,462
  • 2
  • 3
  • 12
  • `?- list_length_freeze(L,L).` – false Feb 21 '23 at 13:20
  • E.g. `?- list_length_freeze(L, L), L = [_|_]` gives `ERROR: Arguments are not sufficiently instantiated` in swi-prolog. Seems reasonable, given that the 2nd argument is (eventually) numeric. – brebs Feb 21 '23 at 14:21
  • Not even that is fine. An instantiation error in your situation is not OK. It is an artefact of `?- X is [_].` producing an instantiation error. Which kind-of is OK for this extension of `(is)/2` but does not make sense in the context of `length/2`. – false Feb 21 '23 at 15:03
  • Added 2 `freeze` checks using `integer`, to produce `false` instead of instantiation error. – brebs Feb 21 '23 at 15:37
  • It does not solve the original problem. – false Feb 21 '23 at 16:20
  • For the case [`length(L,L)`](http://www.complang.tuwien.ac.at/ulrich/iso-prolog/length#22) the most conservative approach is non-termination or resource erros. But certainly, a coroutining/constraint based approach may also add failure. Floundering is not so ideal, in particular with a top level that does not show those floundering goals. Still, floundering is much better than incorrect answers. – false Feb 22 '23 at 05:22
  • Improved: `?- list_length_freeze(L, L).` gives false. – brebs Feb 26 '23 at 10:56
  • Same problem as other answer. – false Feb 26 '23 at 11:06
  • Fixed by using `->` instead of the (surprisingly dangerous) cut. – brebs Feb 26 '23 at 12:11
  • But `?- list_length_freeze(L,N), L = [a|N].` still flounder. – false Feb 26 '23 at 13:29
  • Appending e.g. `N = 1` then properly fails, which is an improvement over swi-prolog and gprolog overflowing. I'm not sure it's an issue worth solving. – brebs Feb 26 '23 at 14:10
  • ISO-wise a resource error [#21](http://www.complang.tuwien.ac.at/ulrich/iso-prolog/length#21) is the only possible answer for `?- length([a,b|N],N).` etc. But implementations may produce this error rapidly. – false Feb 26 '23 at 14:30
  • Otherwise your mixture of eager and lazy answers is at least worth noting. I.e. for `?- length(L, N).` – false Feb 26 '23 at 14:31
-1

Can use -> (implies) with == (compare without instantiation), for determinism:

list_length(Lst, Len) :-
    % Start at zero
    list_length_(Lst, 0, Len).

% Found end of list
list_length_(Lst, Upto, Len) :-
    % Checks for determinism
    (   Lst == [] -> Upto = Len
    ;   Upto == Len -> Lst = []
    % Base case
    ;   Lst = [], Upto = Len
    % Iterate through list
    ;   Lst = [_|T],
        inc_int_below_max(Upto, Len, Upto1),
        list_length_(T, Upto1, Len)
    ).

% Increment, keeping to maximum (if specified)
inc_int_below_max(I, Max, I1) :-
    I1 is I + 1,
    % Check against Max if specified
    (integer(Max) -> I1 @=< Max ; var(Max)).

Results in swi-prolog:

?- list_length([a,b,c], Len).
Len = 3.

?- list_length(Lst, 3).
Lst = [_, _, _].

?- Lst = [a,b,c], list_length(Lst, Len).
Len = 3.

?- Lst = [a,b|Tail], list_length(Lst, 1).
false.

?- freeze(L, (L = [a] ; L = [b])), list_length(L, 1), L = [b].
L = [b].

?- X = N, length([a,b|X], N).
false.

?- freeze(L, (K = 1 ; K = 2)), list_length(L, 0).
L = [],
K = 1 ;
L = [],
K = 2.

This avoids unwanted choicepoints.

brebs
  • 3,462
  • 2
  • 3
  • 12