Why does my list reversal only work correctly in one direction?

103 Views Asked by At

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.

2

There are 2 best solutions below

0
On BEST ANSWER

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.

2
On

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?