Node:Accumulating Lists, Previous:Accumulating Parameters, Up:Last Call Optimization
This technique becomes much more important when extended to lists, as
in this case it can save much building of unneeded lists through
unnecessary calls to append sublists together. For example, the naive
way to reverse a list is:
nreverse([], []). nreverse([H|T], L) :- nreverse(T, L1), append(L1, [H], L).
This is very wasteful, since each call to append/3
copies the initial
part of the list, and adds one element to it. Fortunately, this can be
very easily rewritten to use an accumulating parameter:
reverse(L1, L2) :- reverse(L1, [], L2). % reverse(+X, +Y, -Z) % Z is X reversed, followed by Y reverse([], Z, Z). reverse([H|T], L0, L) :- reverse(T, [H|L0], L).
This version of reverse is many times faster than the naive version, and uses much less memory. The key to understanding the behavior of this predicate is the observation made earlier: using an accumulating parameter, we build the result backwards.
Don't let this confuse you. Building a list forward is easy. For
example, a predicate which returns a list L of consecutive numbers from
1 to N could be written in two different ways: counting up and
collecting the resulting list forward, or counting down and
accumulating the result backward.
iota1(N, L) :- iota1(1, N, L). iota1(N, Max, L) :- ( N > Max -> L = [] ; N1 is N+1, L = [N|L1], iota1(N1, Max, L1) ).
or,
iota2(N, L) :- iota2(N, [], L). iota2(N, L0, L) :- ( N =< 0 -> L = L0 ; N1 is N-1, iota2(N1, [N|L0], L) ).
Both versions generate the same results, and neither waste any space. The second version is slightly faster. Choose whichever approach you prefer.