2
votes

I need help creating a predicate that removes the 2nd to last element of a list and returns that list written in Prolog. So far I have

remove([],[]).
remove([X],[X]).
remove([X,Y],[Y]).

That is as far as I've gotten. I need to figure out a way to recursively go through the list until it is only two elements long and then reassemble the list to be returned. Help with explanation if you can.

2

2 Answers

3
votes

Your definition so far is perfect! It is a little bit too specialized, so we will have to extend it. But your program is a solid foundation.

You "only" need to extend it.

remove([],[]).
remove([X],[X]).
remove([_,X],[X]).
remove([X,_,Y], [X,Y]).
remove([X,Y,_,Z], [X,Y,Z]).
remove([X,Y,Z,_,Z2], [X,Y,Z,Z2]).
...

OK, you see how to continue. Now, let us identify common cases:

...
remove([X,Y,_,Z], [X,Y,Z]).
%       ^^^        ^^^  
remove([X,Y,Z,_,Z2], [X,Y,Z,Z2]).
%       ^^^^^         ^^^^^
...

So, we have a common list prefix. We could say:

Whenever we have a list and its removed list, we can conclude that by adding one element on both sides, we get a longer list of that kind.

remove([X|Xs], [X|Ys]) :-
   remove(Xs,Ys).

Please note that the :- is really an arrow. It means: Provided what is true on the right-hand side, also what is found on the left-hand side will be true.

H-h-hold a minute! Is this really the case? How to test this? (If you test just for positive cases, you will always get a "yes".) We don't have the time to conjure up some test cases, do we? So let us let Prolog do the hard work for us! So, Prolog, fill in the blanks!

remove([],[]).
remove([X],[X]).
remove([_,X],[X]).
remove([X|Xs], [X|Ys]) :-
   remove(Xs,Ys).

| ?- remove(Xs,Ys). % most general goal

Xs = [], Ys = [] ;

Xs = [A], Ys = [A] ;

Xs = [_,A], Ys = [A] ;

Xs = [A], Ys = [A] ;         % redundant, but OK

Xs = [A,B], Ys = [A,B] ;     % WRONG

Xs = [A,_,B], Ys = [A,B] ;

Xs = [A,B], Ys = [A,B] ;     % WRONG again!

Xs = [A,B,C], Ys = [A,B,C] ; % WRONG

Xs = [A,B,_,C], Ys = [A,B,C] ...

It is tempting to reject everything and start again from scratch. But in Prolog you can do better than that, so let's calm down to estimate the actual damage: Some answers are incorrect. And some answers are correct.

It could be that our current definition is just a little bit too general.

To better understand the situation, I will look at the unexpected success remove([1,2],[1,2]) in detail. Who is the culprit for it?

Even the following program slice/fragment succeeds.

remove([],[]).
remove([X],[X]) :- false.
remove([_,X],[X]) :- false.
remove([X|Xs], [X|Ys]) :-
   remove(Xs,Ys).

While this is a specialization of our program it reads: that remove/2 holds for all lists that are the same. That can't be true! To fix the problem we have to do something in the remaining visible part. And we have to specialize it. What is problematic here is that the recursive rule also holds for:

remove([1,2], [1,2]) :-
   remove([2], [2]).
remove([2], [2]) :-
   remove([], []).

That kind of conclusion must be avoided. We need to restrict the rule to those cases were the list has at least two further elements by adding another goal (=)/2.

remove([X|Xs], [Y|Ys]) :-
   Xs = [_,_|_],
   remove(Xs, Ys).

So what was our error? In the informal

Whenever we have a list and its removed list, ...

the term "removed list" was ambiguous. It could mean that we are referring here to the relation remove/2 (which is incorrect, because remove([],[]) holds, but still nothing is removed), or we are referring here to a list with an element removed. Such errors inevitably happen in programming since you want to keep your intuitions afresh by using a less formal language than Prolog itself.

For reference, here again (and for comparison with other definitions) is the final definition:

remove([],[]).
remove([X],[X]).
remove([_,X],[X]).
remove([X|Xs], [X|Ys]) :-
   Xs = [_,_|_],
   remove(Xs,Ys).

There are more efficient ways to do this, but this is the most straight-forward way.

1
votes

I will try to provide another solution which is easier to construct if you only consider the meaning of "second last element", and describe each possible case explicitly:

rem_2nd_last([], []).
rem_2nd_last([First|Rest], R) :-
    rem_2nd_last_2(Rest, First, R). % "Lag" the list once

rem_2nd_last_2([], First, [First]).
rem_2nd_last_2([Second|Rest], First, R) :-
    rem_2nd_last_3(Rest, Second, First, R). % "Lag" the list twice

rem_2nd_last_3([], Last, _SecondLast, [Last]). % End of list: drop second last
rem_2nd_last_3([This|Rest], Prev, PrevPrev, [PrevPrev|R]) :-
    rem_2nd_last_3(Rest, This, Prev, R). % Rest of list

The explanation is hiding in plain view in the definition of the three predicates.

"Lagging" is a way to reach back from the end of the list but keep the predicate always deterministic. You just grab one element and pass the rest of the list as the first argument of a helper predicate. One way, for example, to define last/2, is:

last([H|T], Last) :-
    last_1(T, H, Last).

last_1([], Last, Last).
last_1([H|T], _, Last) :-
    last_1(T, H, Last).