Search code examples
listrecursionprolog

Prolog Recursion in lists, last but one element


The question is to find the last but one character in a list, e.g.

?- last_but_one(X, [a,b,c,d]).
X = c.

My code is:

last_but_one(X, [X,_]).
last_but_one(X, [_|T]) :- last_but_one(X, T).

and the code they gave is:

last_but_one(X, [X,_]).
last_but_one(X, [_,Y|Ys]) :- last_but_one(X, [Y|Ys]).

When I was studying Haskell, I can recall that when questions asked for the 2nd, 3rd, or nth character in some list, the structure was the same as the answer that's been supplied, so I know writing it the way they've written it has some significance. But I still seem to get correct answers with the way I have written it.

Is the way I have written it wrong? Is the code that the guys who made the answer wrote better—and if so, how?


Solution

  • Your original version is much simpler to read. In particular, the recursive rule reads - reading it right-to-left

    last_but_one(X, [_|T]) :- last_but_one(X, T).
                              ^^^^^^^^^^
                                  provided X is the lbo-element in T
    
                           ^^  then, it follows, that (that's an arrow!)
    ^^^^^^^^^^^^^^^^^^^^^^
          X is also the lbo-element of T with one more element
    

    In other words: If you have already an lbo-element in a given list T, then you can construct new lists with any further elements in front that also have the very same lbo-element.

    One might debate which version is preferable as to efficiency. If you are really into that, rather take:

    last_but_one_f1(E, Es) :-
       Es = [_,_|Xs],
       xs_es_lbo(Xs, Es, E).
    
    xs_es_lbo([], [E|_], E).
    xs_es_lbo([_|Xs], [_|Es], E) :-
       xs_es_lbo(Xs, Es, E).
    

    or even:

    last_but_one_f2(E, [F,G|Es]) :-
        es_f_g(Es, F, G, E).
    
    es_f_g([], E, _, E).
    es_f_g([G|Es], _, F, E) :-
       es_f_g(Es, F, G, E).
    

    Never forget general testing:

    ?- last_but_one(X, Es).
       Es = [X,_A]
    ;  Es = [_A,X,_B]
    ;  Es = [_A,_B,X,_C]
    ;  Es = [_A,_B,_C,X,_D]
    ;  Es = [_A,_B,_C,_D,X,_E]
    ;  Es = [_A,_B,_C,_D,_E,X,_F]
    ;  false.
    

    And here are some benchmarks on my olde labtop:

              SICStus     SWI
              4.3.2     7.3.20-1
        --------------+----------+--------
        you   0.850s  |   3.616s |  4.25×
        they  0.900s  |  16.481s | 18.31×
        f1    0.160s  |   1.625s | 10.16×
        f2    0.090s  |   1.449s | 16.10×
        mat   0.880s  |   4.390s |  4.99×
        dcg   3.670s  |   7.896s |  2.15×
        dcgx  1.000s  |   7.885s |  7.89×
        ap    1.200s  |   4.669s |  3.89×
    

    The reason for the big difference is that both f1 and f2 run purely determinate without any creation of a choicepoint.

    Using

    bench_last :-
       \+ ( length(Ls, 10000000),
            member(M, [you,they,f1,f2,mat,dcg,dcgx,ap]), write(M), write(' '),
            atom_concat(last_but_one_,M,P), \+ time(call(P,L,Ls))
       ).