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
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] ? ...
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))
).
I agree with @false that your own version is simpler to read.
Personally, I find using a DCG (see dcg) even easier:
last_but_one(X) --> [X,_].
last_but_one(X) -->
[_],
last_but_one(X).
As interface predicate, you can use:
last_but_one(L, Ls) :-
phrase(last_but_one(L), Ls).
I now would like to add some actual timings.
We have 3 versions for comparison:
last_but_one//1
last_but_one_you/2
last_but_one_they/2
.The test case consists of finding the penultimate element of a list with ten million elements.
We have:
?- length(Ls, 10_000_000), time(last_but_one(L, Ls)), false. 9,999,999 inferences, 1.400 CPU in 1.400 seconds (100% CPU, 7141982 Lips) ?- length(Ls, 10_000_000), time(last_but_one_you(L, Ls)), false. 9,999,998 inferences, 1.383 CPU in 1.383 seconds (100% CPU, 7229930 Lips) ?- length(Ls, 10_000_000), time(last_but_one_they(L, Ls)), false. 9,999,998 inferences, 5.566 CPU in 5.566 seconds (100% CPU, 1796684 Lips)
This shows that not only is the version that they provided much harder to read, it is also by far the slowest for this benchmark.
Always aim for elegance and readability first. Very often, you also obtain a fast version if you follow this principle.
I would say both answers are just as good and I would probably have written it the way you did. What they do in the second solution is that they check, before the recursive call, that the second element is not a empty list ([]). If you trace the two different solutions on the following query: last_but_one(X,[b]).
You'll see that both give the same answer (false), but the second solution takes shorter amount of steps since it returns false before the recursive call is made.
another solution :
code :
last_but_one(R,[X|Rest]):-
( Rest=[_], R=X
; last_but_one(R,Rest)
).
Test :
| ?- last_but_one(Elem,List).
List = [Elem,_A] ? ;
List = [_A,Elem,_B] ? ;
List = [_A,_B,Elem,_C] ? ;
List = [_A,_B,_C,Elem,_D] ? ;
List = [_A,_B,_C,_D,Elem,_E] ? ;
List = [_A,_B,_C,_D,_E,Elem,_F] ? ;
List = [_A,_B,_C,_D,_E,_F,Elem,_G] ? ;
List = [_A,_B,_C,_D,_E,_F,_G,Elem,_H] ?
yes
Hope this idea help you
Here are more ways how you could do it. I wouldn't recommend actually using any of the following methods, but IMO they are interesting as they give a different view on the other codes and on the Prolog library provided by the respective Prolog processors:
In the first three variants, we delegate the "recursive part" to built-in / library predicates:
last_but_one_append(X,Es) :-
append(_, [X,_], Es).
:- use_module(library(lists)).
last_but_one_reverse(X, Es) :-
reverse(Es, [_,X|_]).
last_but_one_rev(X, Es) :-
rev(Es, [_,X|_]). % (SICStus only)
Alternatively, we could use vanilla home-brewed myappend/3
and myreverse/2
:
myappend([], Bs, Bs).
myappend([A|As], Bs, [A|Cs]) :-
myappend(As, Bs, Cs).
last_but_one_myappend(X, Es) :-
myappend(_, [X,_], Es).
myreverse(Es, Fs) :-
same_length(Es, Fs), % for universal termination in mode (-,+)
myreverse_(Es, Fs, []).
myreverse_([], Fs, Fs).
myreverse_([E|Es], Fs, Fs0) :-
myreverse_(Es, Fs, [E|Fs0]).
last_but_one_myreverse(X, Es) :-
myreverse(Es, [_,X|_]).
Let's run the experiments1!
bench_last :-
\+ ( length(Ls, 10000000),
member(M, [you,they,f1,f2,mat,dcg,dcgx,ap,
append,reverse,rev,
myappend,myreverse]),
write(M), write(' '),
atom_concat(last_but_one_,M,P),
\+ time(call(P,_L,Ls))
).
Here are the runtimes2 using SICStus Prolog and SWI-Prolog3,4:
SICStus | SICStus | SWI | 4.3.2 | 4.3.3 | 7.3.20 | -------------------+---------+--------| you 0.26s | 0.10s | 0.83s |3.1×8.3× they 0.27s | 0.12s | 1.03s |3.8×8.5× f1 0.04s | 0.02s | 0.43s |10.8×21.5× f2 0.02s | 0.02s | 0.37s |18.5×18.5× mat 0.26s | 0.11s | 1.02s |3.9×9.0× dcg 1.06s | 0.77s | 1.47s |1.3×1.9× dcgx 0.31s | 0.17s | 0.97s |3.1×5.7× ap 0.23s | 0.11s | 0.42s |1.8×3.8× append 1.50s | 1.13s | 1.57s |1.0×1.3× reverse 0.36s | 0.32s | 1.02s |2.8×3.1× rev 0.04s | 0.04s | --"-- |25.6×25.6× myappend 0.48s | 0.33s | 1.56s |3.2×4.7× myreverse 0.27s | 0.26s | 1.11s |4.1×4.2×
Very impressive! In the SICStus/SWI speedup column, differences > 10% got bold-faced.
Footnote 1: All measurements shown in this answer were obtained on an Intel
Haswell processor
Core i7-4700MQ.
Footnote 2: rev/2
is offered by SICStus—but not by SWI. We compare the fastest "reverse" library predicate.
Footnote 3: The SWI command-line option -G1G
was required to prevent Out of global stack
errors.
Footnote 4: Also, the SWI command-line option -O
(optimize) was tried, but did not yield any improvement.
Here is another approach using DCGs. I think that this solution is much more "graphical", but it seems quite slow in SICStus:
last_but_one_dcg(L, Ls) :-
phrase( ( ..., [L,_] ), Ls).
... --> [].
... --> [_], ... .
So we describe how a list must look like such that it has a last-but-one element. It looks like this: Anything (...
) in front, and then two elements at the end.
It gets a bit faster by expanding phrase/2
. Note that the expansion itself is no longer a conforming program.
last_but_one_dcgx(L, Ls) :-
...(Ls, Ls2),
Ls2 = [L,_].