3

I have produced the following code.

list_reverse([],[]). 
list_reverse([X],[X]).
list_reverse(Ls,[R|Rs]) :-
    last_elem(Ls,R),
    without_last_elem(Ls,Next),
    list_reverse(Next,Rs).

last_elem([E],E).
last_elem([_|Xs],E) :-
    last_elem(Xs,E).

without_last_elem([X,_|[]],[X|[]]).
without_last_elem([X|T0],[X|T1]) :-
    without_last_elem(T0,T1).

Swipl:

?- list_reverse([1,2,3],X).
X = [3, 2, 1] ;
false.

This is exactly what I want.

However if I go in the opposite direction I get success, followed by non-termination.

?- list_reverse(X,[1,2,3]).
X = [3, 2, 1] ;
  C-c C-cAction (h for help) ? a
abort
% Execution Aborted

What I am struggling to understand is why I first get a correct solution for X. Is my program correct or not?

I am not worried about reversing a list as much as I am about this pattern of getting a correct solution followed by non-termination. It is a pattern I have already come across a few times.

false
  • 10,264
  • 13
  • 101
  • 209
Nicholas Hubbard
  • 527
  • 2
  • 15

2 Answers2

3

I am [worried] about this pattern of getting a correct solution followed by non-termination.

This is due to the very specific notion of (universal) termination in Prolog. In other programming languages termination is a much simpler beast (still an undecidable beast nevertheless). If, say, a function returns then it terminates (for that case). But in Prolog, producing an answer is not the end as there might be further solutions or just an unproductive loop. In fact, it's best not to consider your query ?- list_reverse(X,[1,2,3]). but rather the following instead.

?- list_reverse(X,[1,2,3]), false.

In this manner all distracting answers are turned off. The only purpose of this query is now either to show termination or non-termination.

After that, you can either try to follow Prolog's precise execution path but that is as insightful as staring into a car's gearbox when you are lost (the gears caused you to move into the place where you are lost thus they are somehow the cause...). Or, you take a step back, and consider related program fragments (called slices) that share certain properties with your original program. For termination, a helps you to better understand what is at stake. In your case consider:

list_reverse([],[]) :- false. 
list_reverse([X],[X]) :- false.
list_reverse(Ls,[R|Rs]) :-
    last_elem(Ls,R), false,
    without_last_elem(Ls,Next),
    list_reverse(Next,Rs).

last_elem([E],E) :- false.
last_elem([_|Xs],E) :-
    last_elem(Xs,E), false.

?- list_reverse(X,[1,2,3]), false.

Since this failure slice does not terminate, also your original program doesn't terminate! And, it is much easier to reason here in this smaller fragment. If you want to fix the problem, you need to modify something in the visible part. Otherwise you will keep being stuck in a loop.

Note that none of the facts is part of the loop. Thus they are irrelevant for non-termination.

Also note that in list_reverse/2 the variable Rs is never used in the visible part. Thus Rs has no influence on termination! Please note that this is a proof of that property already. Does this mean that the second argument of list_reverse/2 has no influence on termination? What do you think?

false
  • 10,264
  • 13
  • 101
  • 209
  • Using your advice on slicing I realized that the problem is that there are infinite lists with R as the last element. I thought hard about how I could solve the problem without using an accumulator. I could not think about another way to express what it means for an element to the final element of a list. I did however come up with the solution to add a constraint that Ls and [R|Rs] are of the same length. This change gave me the behavior I am looking for, though I think performance would be poor with a very long list. Removing a constraint will at most make a program more general, correct? – Nicholas Hubbard Sep 13 '20 at 12:42
  • *Removing a constraint will at most make a program more general, correct?* True (for pure, monotonic programs). – false Sep 13 '20 at 12:46
2

The last_elem/2 can keep constructing larger lists, that all should be rejected. But you thus get stuck in an infinite loop.

We can make a function that works with accumulator, and iterates over both the two lists concurrently. That means that once the left or right list is exhausted, no more recursive calls will be made:

reverse(L1, L2) :-
    reverse(L1, [], L2, L2).

reverse([], L, L, []).
reverse([H|T], L1, R, [_|T2]) :-
    reverse(T, [H|L1], R, T2).

Here the [H|T] and [_|T2] pattern thus both pop the first item of the list, and we only match if both lists are exhausted.

Willem Van Onsem
  • 443,496
  • 30
  • 428
  • 555