List

advertisement
Part 1 The Prolog Language
Chapter 8
Programming Style and
Technique
1
8.1 General principles of good
programming

What is a good program?
 Generally accepted criteria include the
following:
Correctness
 User-friendliness
 Efficiency
 Readability
 Modifiability
 Robustness
 Documentation

2
8.2 How to think about Prolog
programs


During the process of developing a solution
we have to find ideas for reducing problems
to one or more easier subproblems.
How do we find proper subproblems?



Use of recursion (Section 8.2.1)
Generalization (Section 8.2.2)
Using pictures (Section 8.2.3)
3
8.2.1 Use of recursion

The principle here is to split the problem into two
cases:
(1) trivial, or boundary cases;
(2) general cases where the solution is constructed(建構)
from solutions of (simpler) versions of the original
program itself.

An example:

Processing a list of items so that each item is
transformed by the same transformation rule.
maplist( List, F, NewList)
where List is an original list, F is a transformation rule
and NewList is the list of all transformed items.
4
8.2.1 Use of recursion

The problem of transforming List can be split into two
cases:
(1) Boundary case: List = []
if List = [] then NewList = [], regardless(不管) of F
(2) General case: List = [X|Tail]
To transform a list of the form [X|Tail] do:
transform the item X by rule F obtaining NewX, and
transform the list Tail obtaining NewTail;
the whole transformed list is [NewX|NewTail].

In Prolog:
maplist( [], _, []).
maplist( [X|Tail], F, [NewX|NewTail]) :G =.. [F, X, NewX], call( G), maplist(Tail, F, NewTail).
5
8.2.1 Use of recursion

Suppose we have a list of numbers and want to
compute the list of their squares.
square( X, Y) :- Y is X * X.
maplist( [], _, []).
maplist( [X|Tail], F, [NewX|NewTail]) :G =.. [F, X, NewX],
call( G),
maplist(Tail, F, NewTail).
| ?- maplist([2,6,5], square, Square).
Square = [4,36,25]
yes
6
8.2.2 Generalization



It is often a good idea to generalize the original
problem, so that the solution to the generalized
problem can be formulated recursively.
The original problem is then solved as a special case
of its more general version.
The example is the eight queens problem.


The original problem was to place eight queens on the
chessboard so that they do not attack each other.
eightqueens( Pos)
This is true if Pos is a position with eight non-attacking
queens.
A good idea in this case is to generalize the number of
queens from eight to N.
nqueens( Pos, N)
7
8.2.2 Generalization

The advantage of this generalization is that there is
an immediate recursive formulation of the nqueens
relation:
(1) Boundary case: N = 0
To safely place zero queens is trivial.
(2) General case: N > 0
To safely place N queens on the board, satisfy
the following:
 Achieve a safe configuration of (N-1) queens;
and
 Add the remaining queen so that she does not
attach any other queen.
eightqueens( Pos) :- nqueens( Pos, 8)
8
4.5.3 The eight queens problem—
Program 3
-2
-7
+7
u=x-y
y
8

7
6
5
4
●
3
2
1
1
2
3
4
5
6
7
8
x
The domains for all
four dimensions are:
Dx=[1,2,3,4,5,6,7,8]
Dy=[1,2,3,4,5,6,7,8]
Du=[-7,-6,-5,-4,-3,-2,
-1,0,1,2,3,4,5,6,7]
Dv=[2,3,4,5,6,7,8,9,10,
11,12,13,14,15,16]
v=x+y
2
6
16
9
4.5.3 The eight queens problem—
Program 3
% Figure 4.11
Program 3 for the eight queens problem.
solution( Ylist) :sol( Ylist, [1,2,3,4,5,6,7,8], [1,2,3,4,5,6,7,8],
[-7,-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6,7],
[2,3,4,5,6,7,8,9,10,11,12,13,14,15,16] ).
sol( [], [], Dy, Du, Dv).
sol( [Y | Ylist], [X | Dx1], Dy, Du, Dv) :del( Y, Dy, Dy1), U is X-Y, del( U, Du, Du1), V is X+Y,
del( V, Dv, Dv1), sol( Ylist, Dx1, Dy1, Du1, Dv1).
del( Item, [Item | List], List).
del( Item, [First | List], [First | List1] ) :del( Item, List, List1).
10
4.5.3 The eight queens problem—
Program 3



To generation of the domains:
gen( N1, N2, List)
which will, for two given integers N1 and N2, produce the
list
List = [ N1, N1+1, N1+2, ..., N2-1, N2]
Such procedure is:
gen( N, N, [N]).
gen( N1, N2, [N1|List]) :N1 < N2, M is N1+1, gen(M, N2, List).
The gereralized solution relation is:
solution( N, S) :gen(1, N, Dxy), Nu1 is 1-N, Nu2 is N-1,
gen(Nu1, Nu2, Du), Nv2 is N+N,
gen(2, Nv2, Dv), sol( S, Dxy, Dxy, Du, Dv).
11
4.5.3 The eight queens problem—
Program 3

For example, a solution to the 12-queens probelm would
be generated by:
?- solution( 12, S).
S=[1,3,5,8,10,12,6,11,2,7,9,4]
12
8.2.3 Using pictures



When searching for ideas about a problem, it is
often useful to introduce some graphical
representation of the problem.
A picture may help us to perceive(理解) some
essential relations in the problem.
The use of pictorial representations is very useful in
Prolog.


Prolog is particularly suitable for problems that involve
objects and relations between objects. Such problem
can be naturally illustrated by graph.
Structured data objects in Prolog are naturally pictured
as trees.
13
2.1.3 Structures

Tree representation of the objects:
P1 = point( 1, 1)
S = seg( P1, P2)
= seg( point(1,1), point(2,3))
T = triangle( point(4,2), point(6,4), point(7,1))
Principal factor
P1=point
1
S=seg
1
point
1
1
T=triangle
point
2
3
point
4
2
point
6
4
point
7
1
14
8.3 Programming style

The purpose of conforming to some stylistic(文體的)
conventions(慣例) is:


To reduce the danger of programming errors;
and
To produce programs that are


readable and easy to understand
easy to debug and to modify
15
8.3.1 Some rules of good style




Program clauses should be short.
Procedures should be short because long procedures are
hard to understand.
Mnemonic(有助記憶的) names for procedures and variables
should be used.
The layout(版面編排) of programs is important.




Spacing, blank lines and indentation(縮排) should be
consistently used for the sake(目的) of readability.
Clauses about the same procedure should be clustered
together.
There should be blank lines between clauses.
Stylistic conventions of this kind may vary from program
to program. However, it is important that the same
conventions are used consistently throughout the whole
program.
16
8.3.1 Some rules of good style

The cut operator should be used with care.



The not procedure can also lead to surprising
behavior, as it is related to cut.


Cut should not be used if it can be easily avoided.
It is better to use ‘green cuts’ rather than ‘red cuts’.
If there is a dilemma(兩難的選擇) between not and cut,
the former is perhaps better than some obscure(模糊的)
construct with cut.
Program modification by assert and retract can
grossly(非常地) degrade the transparency (降低透明度)
of the program’s behavior.

In particular, the same program will answer the same
question differently at different times.
17
8.3.1 Some rules of good style

The use of a semicolon(;) may obscure(使不顯著) the
meaning of a clause.


The readability can sometimes be improved by
splitting the clause containing the semicolon into more
clauses.
To illustrate some points of this section, consider the
relation
merge( List1, List2, List3)
where List1 and List2 are ordered lists that merge
into List3.

For example:
merge([2,4,7], [1,3,4,8], [1,2,3,4,4,7,8])
18
8.3.1 Some rules of good style

A bad style
merge( List1, List2, List3) :List1 = [], !, List3 = List2;
List2 = [], !, List3 = List1;
List1 = [X|Rest1],
List2 = [Y|Rest2],
( X < Y, !,
Z = X,
merge( Rest1, List2, Rest3);
Z = Y,
merge( List1, Rest2, Rest3)),
List3 = [Z| Rest3].
19
8.3.1 Some rules of good style

A better version
merge1( [], List, List) :- !.
merge1( List, [], List).
merge1( [X|Rest1], [Y|Rest2], [X|Rest3]) :X < Y, !,
merge1( Rest1, [Y|Rest2], Rest3).
merge1( List1, [Y|Rest2], [Y|Rest3]) :merge1( List1, Rest2, Rest3).
20
8.3.1 Some rules of good style
| ?- merge1([2,4,7],[1,3,4,8], List).
List = [1,2,3,4,4,7,8]
yes
| ?- merge([2,4,7],[1,3,4,8], List).
List = [1,2,3,4,4,7,8]
Yes
| ?- merge1([2], [8], [2,8]).
yes
| ?- merge([2], List, [2,8]).
no
| ?- merge1([2], List, [2,8]).
uncaught exception: error(instantiation_error,(<)/2)
21
8.3.2 Tabular organization of long
procedures



Long procedures are acceptable if they have some
uniform structure.
Such a form is a set of facts when a relation is
effectively defined in the tabular(列表的) form.
The advantages of such an organization of a long
procedure are:



Its structure is easily understood.
Incrementability: it can be refined by simply adding
new facts.
It is easy to check and correct or modify by simply
replacing some fact independently of other facts.
22
8.3.3 Commenting


The main purpose of comments is to enable the user
to use the program, to understand it and to possibly
modify it.
Long passages(一段) of comments should precede
the code they refer to, while short comments should
be interspersed with the code itself.
23
8.4 Debugging


The basis for debugging aids is tracing.
‘Tracing a goal’ means that the information regarding the
goal’s satisfaction is displayed during execution. This
information includes:





Entry information
Exit information
Re-entry information
Such debugging aids are activated by system-dependent
built-in predicates.
A typical subset of such predicates is as follows:
trace: trigger exhaustive tracing of goals that follow.
notrace: stop further tracing.
spy( P): specifies that a predicate P be traced.
nospy( P): stops spying P.
24
8.4 Debugging
| ?- trace.
The debugger will first creep -- showing everything (trace)
yes
{trace}
| ?- merge1([2], [8], [2,8]).
1 1 Call: merge1([2],[8],[2,8]) ?
2 2 Call: 2<8 ?
2 2 Exit: 2<8 ?
3 2 Call: merge1([],[8],[8]) ?
3 2 Exit: merge1([],[8],[8]) ?
1 1 Exit: merge1([2],[8],[2,8]) ?
yes
{trace}
| ?- merge([2], [8], [2,8]).
1 1 Call: merge([2],[8],[2,8]) ?
2 2 Call: 2<8 ?
2 2 Exit: 2<8 ?
3 2 Call: merge([],[8],_114) ?
3 2 Exit: merge([],[8],[8]) ?
1 1 Exit: merge([2],[8],[2,8]) ?
(15 ms) yes
{trace}
| ?- notrace.
The debugger is switched off
yes
25
8.4 Debugging
| ?- spy( merge).
Spypoint placed on merge/3
The debugger will first leap -- showing spypoints (debug)
(15 ms) yes
{debug}
| ?- merge([2], [8], [2,8]).
+ 1 1 Call: merge([2],[8],[2,8]) ?
2 2 Call: 2<8 ?
2 2 Exit: 2<8 ?
+ 3 2 Call: merge([],[8],_114) ?
+ 3 2 Exit: merge([],[8],[8]) ?
+ 1 1 Exit: merge([2],[8],[2,8]) ?
yes
{debug}
| ?- merge1([2], [8], [2,8]).
yes
{debug}
| ?- nospy( merge).
Spypoint removed from merge/3
yes
{debug}
26
8.5 Improving efficiency


Ideas for improving the efficiency of a program
usually come from a deeper understanding of the
problem.
A more efficient algorithm can result from
improvements of two kinds:


Improving search efficiency by avoiding unnecessary
backtracking and stopping the execution of useless
alternatives as soon as possible.
Using more suitable data structures to represent
objects in the program, so that operations on objects
can be implemented more efficiently.
27
8.5.1 Improving the efficiency of an
eight queens program



In the program of Figure 4.7:
member( Y, [1,2,3,4,5,6,7,8])
The queens in adjacent columns will attach each
other if they are not placed at least two squares
apart in the vertical direction.
According to this observation, we can rearrange the
candidate coordinate values to improve the
efficiency:
member( Y, [1,5,2,6,3,7,4,8])
28
8.5.1 Improving the efficiency of an
eight queens program
% Figure 4.7 Program 1 for the eight queens problem.
solution( [] ).
solution( [X/Y | Others] ) :solution( Others), member( Y, [1,2,3,4,5,6,7,8] ),
noattack( X/Y, Others).
member( Y, [1,5,2,6,3,7,4,8] ),
noattack( _, [] ).
noattack( X/Y, [X1/Y1 | Others] ) :Y =\= Y1, Y1-Y =\= X1-X, Y1-Y =\= X-X1, noattack( X/Y,
Others).
member( Item, [Item | Rest] ).
member( Item, [First | Rest] ) :- member( Item, Rest).
% A solution template
template( [1/Y1,2/Y2,3/Y3,4/Y4,5/Y5,6/Y6,7/Y7,8/Y8] ).
29
8.5.2 Improving the efficiency in a map
coloring program


The map coloring problem is to assign each country
in a given map one of four given colors in such a
way that no two neighboring countries are painted
with the same color.
Assume that a map is specified by the neighbor
relation
ngb( Country, Neighbors)
where Neighbors is the list of countries bordering on
Country.

So the map of Europe, with 30 countries, would be
specified as:
ngb( albania(阿爾巴尼亞), [greece(希臘), macedonia(馬其頓),
yugoslavia(南斯拉夫)]).
ngb( andorra(安道爾共和國), [france, spain]).
… (see http://www.csie.ntnu.edu.tw/~violet)
30
8.5.2 Improving the efficiency in a map
coloring program


For a given map, the names of countries are fixed in
advance, and the problem is to find the values for the
colors.
The problem is to find a proper instantiation of variables
C1, C2, C3, etc. in the list:
[albania/C1, andorra/C2, austria/C3,…]
ngb( albania(阿爾巴尼亞), [greece(希臘), macedonia(馬其頓), yugoslavia(南斯拉夫)]).
ngb( andorra(安道爾共和國), [france, spain]).
…
31
8.5.2 Improving the efficiency in a map
coloring program



Define the predicate
colors( Country_color_list)
which is true if the Country_color_list satisfies the map
coloring constraint with respect to a given ngb relation.
Let the four colors be yellow, blue, red and green.
The condition that no two neighboring countries are of the
same color can be formulated in Prolog as follows:
colors([]).
colors([ Country/Color | Rest]) :colors( Rest),
member( Color, [yellow, blue, red, green]),
not( member( Country1/Color, Rest),
neighbor( Country, Country1)).
neighbor( Country, Country1) :ngb( Country, Neighbors), member( Country1, Neighbors).
32
8.5.2 Improving the efficiency in a map
coloring program

Assuming that the built-in predicate setof is available,
one attempt to color Europe could be as follows.

Define the relation
country( C) :- ngb( C, _)

Then the question for coloring Europe can be formulated
as:
?- setof( Cntry/Color, country( Cntry), CountryColorList),
colors( CountryColorList).
 The setof goal will construct a template country/color list
for Europe in which uninstantiated variables stand for
colors.
 Then the colors goal is supposed to instantiate the color.

However, this attempt will probably fail because of
inefficiency.
33
8.5.2 Improving the efficiency in a map
coloring program

For example:
ngb( albania, [greece]).
ngb( greece, [albania]).
ngb( andorra, [france, spain]).
ngb( france, [andorra, spain]).
ngb( spain, [andorra, france]).
country( C) :- ngb( C, _).
colors([]).
colors([ Country/Color | Rest]) :colors( Rest),
member( Color, [yellow, blue, red, green]),
not((member( Country1/Color, Rest),
neighbor( Country, Country1))).
neighbor( Country, Country1) :ngb( Country, Neighbors), member( Country1, Neighbors).
34
8.5.2 Improving the efficiency in a map
coloring program
| ?- setof( Cntry/Color, country( Cntry), CountryColorList).
CountryColorList = [albania/_,andorra/_,france/_,greece/_,spain/_]
Yes
| ?- setof( Cntry/Color, country( Cntry), CountryColorList),
colors( CountryColorList).
CountryColorList
CountryColorList
CountryColorList
CountryColorList
CountryColorList
CountryColorList
CountryColorList
CountryColorList
CountryColorList
…
=
=
=
=
=
=
=
=
=
[albania/blue,andorra/red,france/blue,greece/yellow,spain/yellow] ? ;
[albania/red,andorra/red,france/blue,greece/yellow,spain/yellow] ? ;
[albania/green,andorra/red,france/blue,greece/yellow,spain/yellow] ? ;
[albania/blue,andorra/green,france/blue,greece/yellow,spain/yellow] ? ;
[albania/red,andorra/green,france/blue,greece/yellow,spain/yellow] ? ;
[albania/green,andorra/green,france/blue,greece/yellow,spain/yellow] ? ;
[albania/blue,andorra/blue,france/red,greece/yellow,spain/yellow] ? ;
[albania/red,andorra/blue,france/red,greece/yellow,spain/yellow] ? ;
[albania/green,andorra/blue,france/red,greece/yellow,spain/yellow] ? ;
ngb(
ngb(
ngb(
ngb(
ngb(
albania, [greece]).
greece, [albania]).
andorra, [france, spain]).
france, [andorra, spain]).
spain, [andorra, france]).
35
8.5.2 Improving the efficiency in a map
coloring program

Why inefficiency?





Countries in the country/color list are arranged in
alphabetical(照字母次序的) order, and this has nothing to do
with their geographical(地理的) arrangement.
This may easily lead to a situation in which a country that is to
be colored is surrounded by many other countries, already
painted with all four available colors.
Then backtracking is necessary, which leads to inefficiency.
It is clear that the efficiency depends on the order in which the
countries are colored.
Suggestion: start with some country that has many neighbors,
and then proceed to the neighbors, then to the neighbors of
neighbors, etc.
 For example: Germany has most neighbors in Europe.
36
8.5.2 Improving the efficiency in a map
coloring program





The following procedure, makelist, can construct a properly ordered list of
countries.
Germany has to be put at the end of the list and other countries have to be
added at the front of the list.
It starts the construction with some specified country (Germany in our case)
and collects the countries into a list called Closed.
Each country is first put into another list, called Open, before it is
transferred to Closed.
Each time that a country is transferred from Open to Closed, its neighbors
are added to Open.
makelist( List):- collect( [germany], [], List).
collect([], Closed, Closed).
collect([ X | Open], Closed, List):member( X, Closed),!, collect( Open, Closed, List).
collect([ X | Open], Closed, List):ngb( X, Ngbs), conc( Ngbs, Open, Open1),
collect( Open1, [X|Closed], List).
37
8.5.2 Improving the efficiency in a map
coloring program
ngb(
ngb(
ngb(
ngb(
ngb(
ngb(
albania,[greece]).
greece, [albania, germany]).
andorra,[france, germany, spain]).
france, [andorra, germany, spain]).
spain, [andorra, france, germany]).
germany, [andorra, france, greece, spain]).
con_list([], L).
con_list( [X| L1], [X/_|L3]) :- con_list( L1, L3).
makelist( List):- collect( [germany], [], List).
collect([], Closed, Closed).
collect([ X | Open], Closed, List):member( X, Closed),!, collect( Open, Closed, List).
collect([ X | Open], Closed, List):ngb( X, Ngbs), conc( Ngbs, Open, Open1),
collect( Open1, [X|Closed], List).
38
8.5.2 Improving the efficiency in a map
coloring program
| ?- makelist( L).
L = [albania,greece,spain,france,andorra,germany]
Yes
| ?- makelist( L), con_list( L, L1).
L = [albania,greece,spain,france,andorra,germany]
L1 = [albania/_,greece/_,spain/_,france/_,andorra/_,germany/_|_]
(16 ms) yes
| ?- makelist( L), con_list( L, L1), colors( L1).
L = [albania,greece,spain,france,andorra,germany]
L1 = [albania/yellow,greece/blue,spain/green,france/red,andorra/blue,germany/yellow] ? ;
L = [albania,greece,spain,france,andorra,germany]
L1 = [albania/red,greece/blue,spain/green,france/red,andorra/blue,germany/yellow] ? ;
L = [albania,greece,spain,france,andorra,germany]
L1 = [albania/green,greece/blue,spain/green,france/red,andorra/blue,germany/yellow] ? ;
L = [albania,greece,spain,france,andorra,germany]
L1 = [albania/yellow,greece/red,spain/green,france/red,andorra/blue,germany/yellow] ? ;
L = [albania,greece,spain,france,andorra,germany]
L1 = [albania/blue,greece/red,spain/green,france/red,andorra/blue,germany/yellow] ? ;
L = [albania,greece,spain,france,andorra,germany]
L1 = [albania/green,greece/red,spain/green,france/red,andorra/blue,germany/yellow] ? ;
L = [albania,greece,spain,france,andorra,germany]
L1 = [albania/yellow,greece/green,spain/green,france/red,andorra/blue,germany/yellow] ?
…
39
8.5.3 Improving efficiency of list
concatenation by difference lists

In our programs so far, the concatenation of list has been
programmed as:
conc([], L, L).
conc([X| L1], L2, [X| L3]) :- conc( L1, L2, L3).


This is inefficient when the first list is long.
The following example explains why?
?- conc([a,b,c],[d,e],L).
This produces the following sequence of goals:
conc([a,b,c],[d,e],L)
conc([b,c],[d,e],L’)
where L = [a|L’]
conc([c],[d,e],L’’)
where L’ = [b|L’’]
conc([],[d,e],L’’’) where L’’ = [c|L’’’]
true
where L’’’ = [d,e]
{trace}
| ?- conc([a,b,c],[d,e],L).
1 1 Call: conc([a,b,c],[d,e],_26) ?
2 2 Call: conc([b,c],[d,e],_59) ?
3 3 Call: conc([c],[d,e],_86) ?
4 4 Call: conc([],[d,e],_113) ?
4 4 Exit: conc([],[d,e],[d,e]) ?
3 3 Exit: conc([c],[d,e],[c,d,e]) ?
2 2 Exit: conc([b,c],[d,e],[b,c,d,e]) ?
1 1 Exit: conc([a,b,c],[d,e],[a,b,c,d,e]) ?
L = [a,b,c,d,e]
(62 ms) yes
40
{trace}
8.5.3 Improving efficiency of list
concatenation by difference lists





The program scans all of the first list until the empty list is
encountered.
If we could simply skip the whole of the first list in a single
step, then the program will be more efficient.
To do this, we need to know where the end of a list is; that
is, we need another representation of lists.
One solution is the data sturcture called difference lists.
For example:
 The list [a,b,c] can be represented by the two lists:
L1 = [a,b,c,d,e]
L2 = [d,e]

Such a pair of lists, L1 – L2, represents the ‘difference’
between L1 and L2.
This only works under the condition that L2 is a suffix of
L1.
41
8.5.3 Improving efficiency of list
concatenation by difference lists


Note the same list can be represented by
several ’difference pairs’.
For example: the list [a,b,c] can be represented by
[a,b,c] – []
or [a,b,c,d,e] – [d,e]
or [a,b,c,d,e|T] – [d,e|T]
or [a,b,c|T] – [T]



where T is any list.
The empty list is represented by any pair of the form L – L.
As the second member of the pair indicates the end of the
list, the end is directly accessible.
This can be used for an efficient implementation of
concatenation.
42
8.5.3 Improving efficiency of list
concatenation by difference lists
Z1 A2
A1
L1
Z2
L2
L3

The corresponding concatenation relation translates
into Prolog as the fact:
concat( A1-Z1, Z1-Z2, A1-Z2)
?- concat([a,b,c|T1]-T1, [d,e|T2]-T2, L).
T1 = [d,e|T2]
L = [a,b,c,d,e|T2]-T2
(concat is not a built-in predicate in GNU Prolog)
43
8.5.4 Last call optimization and
accumulators





Recursive call normally take up memory space, which
is only freed after the return from the call.
A large number of nested recursive calls may lead to
shortage of memory.
In special cases, it is possible to execute nested
recursive calls without requiring extra memory.
In such a case a recursive procedure has a special
form, call tail recursion.
A tail-recursive procedure


It only has one recursive call, and the call appears as
the last goal of the last clause in the procedure.
The goals preceding the recursive call must be
deterministic, so that no backtracking occurs after this
last call.
44
8.5.4 Last call optimization and
accumulators





Typically a tail-recursive procedure looks like this:
p(...) :- ...
% No recursive call in the body of this clause
p(...) :- ...
% No recursive call in the body of this clause
p(...) :- ..., !, % The cut ensure no backtracking
p(...). % Tail-recursive call
In the cases of such tail-recursive procedures, no information is
needed upon the return from a call.
Therefore such recursion can be carried out simply as iteration in
which a next cycle in the loop does not require additional memory.
A Prolog system will notice such an opportunity of saving memory
and realize tail recursion as iteration.
This is called tail recursion optimization, or last call optimization.
45
8.5.4 Last call optimization and
accumulators

For example:
Consider the predicate for computing the sum of a list of numbers
sumlist( List, Sum)
It can be defined as:
sumlist([], 0).
sumlist([First |Rest], Sum) :sumlist( Rest, Sum0), Sum is First + Sum0.
 This is not tail recursive, so the summation over a very long
list will require many recursive calls and therefore a lot of
memory.
sumlist1( List, Sum) :- sumlist1( List, 0, Sum).
sumlist1([], Sum, Sum).
sumlist1([First|Rest], PartialSum, TotalSum) :NewPartialSum is PartialSum + First,
sumlist1( Rest, NewPartialSum, TotalSum).
 This is now tail recursive and Prolog can benefit from last call
optimization.
46
8.5.4 Last call optimization and
accumulators
{trace}
| ?-sumlist([1,3,5,7], Sum).
1 1 Call: sumlist([1,3,5,7],_24) ?
2 2 Call: sumlist([3,5,7],_93) ?
3 3 Call: sumlist([5,7],_117) ?
4 4 Call: sumlist([7],_141) ?
5 5 Call: sumlist([],_165) ?
5 5 Exit: sumlist([],0) ?
6 5 Call: _193 is 7+0 ?
6 5 Exit: 7 is 7+0 ?
4 4 Exit: sumlist([7],7) ?
7 4 Call: _222 is 5+7 ?
7 4 Exit: 12 is 5+7 ?
3 3 Exit: sumlist([5,7],12) ?
8 3 Call: _251 is 3+12 ?
8 3 Exit: 15 is 3+12 ?
2 2 Exit: sumlist([3,5,7],15) ?
9 2 Call: _24 is 1+15 ?
9 2 Exit: 16 is 1+15 ?
1 1 Exit: sumlist([1,3,5,7],16) ?
Sum = 16
yes
{trace}
{trace}
| ?- sumlist1([1,3,5,7], Sum).
1 1 Call: sumlist1([1,3,5,7],_24) ?
2 2 Call: sumlist1([1,3,5,7],0,_24) ?
3 3 Call: _121 is 0+1 ?
3 3 Exit: 1 is 0+1 ?
4 3 Call: sumlist1([3,5,7],1,_24) ?
5 4 Call: _174 is 1+3 ?
5 4 Exit: 4 is 1+3 ?
6 4 Call: sumlist1([5,7],4,_24) ?
7 5 Call: _227 is 4+5 ?
7 5 Exit: 9 is 4+5 ?
8 5 Call: sumlist1([7],9,_24) ?
9 6 Call: _280 is 9+7 ?
9 6 Exit: 16 is 9+7 ?
10 6 Call: sumlist1([],16,_24) ?
10 6 Exit: sumlist1([],16,16) ?
8 5 Exit: sumlist1([7],9,16) ?
6 4 Exit: sumlist1([5,7],4,16) ?
4 3 Exit: sumlist1([3,5,7],1,16) ?
2 2 Exit: sumlist1([1,3,5,7],0,16) ?
1 1 Exit: sumlist1([1,3,5,7],16) ?
Sum = 16
yes
{trace}
47
8.5.4 Last call optimization and
accumulators

Another example:
reverse( List, ReversedList)
ReversedList has the same elements as List, but in the reverse
order.
It can be defined as:
reverse([], []).
reverse([X |Rest], Reversed) :reverse( Rest, RevRest), conc( RevRest, [X], Reversed).
 This is not tail recursive.
 The program is very inefficient because to reverse a list of
length n, it require time proportional to n2.
reverse1( List, Reversed) :- reverse1( List, [], Reversed).
reverse1([], Reversed, Reversed).
reverse1([X|Rest], PartReversed, TotalReversed) :reverse1( Rest, [X|PartReversed], TotalReversed).
 This is efficient and tail recursive.
48
8.5.4 Last call optimization and
accumulators
| ?- reverse([1,3,5,7], List).
1 1 Call: reverse([1,3,5,7],_24) ?
2 2 Call: reverse([3,5,7],_93) ?
3 3 Call: reverse([5,7],_117) ?
4 4 Call: reverse([7],_141) ?
5 5 Call: reverse([],_165) ?
5 5 Exit: reverse([],[]) ?
6 5 Call: conc([],[7],_193) ?
6 5 Exit: conc([],[7],[7]) ?
4 4 Exit: reverse([7],[7]) ?
7 4 Call: conc([7],[5],_222) ?
8 5 Call: conc([],[5],_209) ?
8 5 Exit: conc([],[5],[5]) ?
7 4 Exit: conc([7],[5],[7,5]) ?
3 3 Exit: reverse([5,7],[7,5]) ?
9 3 Call: conc([7,5],[3],_279) ?
10 4 Call: conc([5],[3],_266) ?
11 5 Call: conc([],[3],_293) ?
11 5 Exit: conc([],[3],[3]) ?
10 4 Exit: conc([5],[3],[5,3]) ?
9 3 Exit: conc([7,5],[3],[7,5,3]) ?
2 2 Exit: reverse([3,5,7],[7,5,3]) ?
12 2 Call: conc([7,5,3],[1],_24) ?
13 3 Call: conc([5,3],[1],_351) ?
14 4 Call: conc([3],[1],_378) ?
15 5 Call: conc([],[1],_405) ?
15 5 Exit: conc([],[1],[1]) ?
14 4 Exit: conc([3],[1],[3,1]) ?
13 3 Exit: conc([5,3],[1],[5,3,1]) ?
12 2 Exit: conc([7,5,3],[1],[7,5,3,1]) ?
1 1 Exit: reverse([1,3,5,7],[7,5,3,1]) ?
List = [7,5,3,1]
yes
{trace}
| ?- reverse1([1,3,5,7], List).
1 1 Call: reverse1([1,3,5,7],_24) ?
2 2 Call: reverse1([1,3,5,7],[],_24) ?
3 3 Call: reverse1([3,5,7],[1],_24) ?
4 4 Call: reverse1([5,7],[3,1],_24) ?
5 5 Call: reverse1([7],[5,3,1],_24) ?
6 6 Call: reverse1([],[7,5,3,1],_24) ?
6 6 Exit: reverse1([],[7,5,3,1],[7,5,3,1]) ?
5 5 Exit: reverse1([7],[5,3,1],[7,5,3,1]) ?
4 4 Exit: reverse1([5,7],[3,1],[7,5,3,1]) ?
3 3 Exit: reverse1([3,5,7],[1],[7,5,3,1]) ?
2 2 Exit: reverse1([1,3,5,7],[],[7,5,3,1]) ?
1 1 Exit: reverse1([1,3,5,7],[7,5,3,1]) ?
List = [7,5,3,1]
yes
{trace}
49
8.5.5 Simulating arrays with arg





The list structure is the easiest representation for sets
in Prolog.
However, accessing an item in a list is done by
scanning the list.
For long lists this is very inefficient.
In such cases, array structures are the most effective
because they enable direct access to a required
element.
There is no array facility in Prolog, but array can be
simulated to some extent by using the built-in
predicates arg and functor.
50
8.5.5 Simulating arrays with arg


The goal
functor( A, f, 100)
make a structure with 100 elements:
A = f(_, _, _, ...)
The goal
arg( 60, A, 1)
means the initial value of the 60th element of array A
is 1. ( A[60] := 1)

Then, arg (60, A, X) means X := A[60].
51
8.5.5 Simulating arrays with arg

For example: the eight queens problem in Chapter 4
(Figure 4.11)
solution( Ylist) :sol( Ylist, [1,2,3,4,5,6,7,8], [1,2,3,4,5,6,7,8],
[-7,-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6,7], % Du
[2,3,4,5,6,7,8,9,10,11,12,13,14,15,16] ).
sol( [], [], Dy, Du, Dv).
sol( [Y | Ylist], [X | Dx1], Dy, Du, Dv) :del( Y, Dy, Dy1), U is X-Y, del( U, Du, Du1), V is X+Y,
del( V, Dv, Dv1), sol( Ylist, Dx1, Dy1, Du1, Dv1).
del( Item, [Item | List], List).
del( Item, [First | List], [First | List1] ) :del( Item, List, List1).
52
8.5.5 Simulating arrays with arg

For example: the eight queens problem in Chapter 4
(Figure 4.11)




The program places a next queen into a currently free
column (X-coordinate), row (Y-coordinate), upward
diagonal (U-coordinate) and downward diagonal( Vcoordinate).
The sets of currently free coordinates are maintained,
and when a new queen is placed the corresponding
occupied coordinates are deleted from these sets.
The deletion of U and V coordinates in Figure 4.11
involves scanning the corresponding lists, which is
inefficient.
Efficiency can easily be improved by simulated arrays.
53
8.5.5 Simulating arrays with arg

For example: the eight queens problem in Chapter 4
(Figure 4.11)

The set of all 15 upward diagonals can be represented
by:
Du = u(_,_,_,_,_,_,_,_,_,_,_,_,_,_,_)
[-7,-6,-5,-4,-3,-2,-1,0, 1, 2, 3, 4, 5, 6, 7], % Du

Consider placing a queen at the square (X, Y) = (1,1).
 u = X-Y = 0
 the 8th component of Du is set to 1
 arg( 8, Du, 1) % Here X = 1.
 Du = u(_,_,_,_,_,_,_,1,_,_,_,_,_,_,_)
 if later a queen is attempted to be placed at (X,Y)=(3,3)
 u = X-Y = 0
 arg( 8, Du, 3) % Here X = 3.
 This will fail beacuse the 8th component of Du is already 1.
So the program will not allow another queen to be placed on
the same diagonal.
54
8.5.6 Improving efficiency by asserting
derived facts



Sometimes during computation the same goal has to be satisfied
again and again.
As Prolog has no special machanism to discover such situations
whole computation sequences are repeated.
For example, consider a program to computer the Nth Fibonacci
number for a given N.
 The Fibonacci sequence is:
1, 1, 2, 3, 5, 8, 13,...
 Each number in the squence is the sum of the previous two
number.
 We can define a predicate
fib( N, F)
fib(1,1).
fib(2,1).
fib( N, F) :- N > 2, N1 is N – 1, fib(N1, F1),
N2 is N – 2, fib(N2, F2), F is F1 + F2.
55
8.5.6 Improving efficiency by asserting
derived facts
| ?- fib( 6, F).
F=8?;
(16 ms) no
f(6)
+
f(5)
f(4)
+
+
f(4)
f(3)
f(3)
f(2)
+
+
+
1
f(3)
f(2)
f(2)
f(1)
f(2)
f(1)
+
1
1
1
1
1
f(2)
f(1)
1
1
In this example, the third Fibonacci number, f(3),
is needed in three places and the same computation
is repeated each time.
56
8.5.6 Improving efficiency by asserting
derived facts

A better idea is to use the built-in procedure asserta and
to add this results as facts to the database.
fib2(1,1).
fib2(2,1).
fib2( N, F) :- N > 2, N1 is N – 1, fib2(N1, F1),
N2 is N – 2, fib2(N2, F2),
F is F1 + F2,
asserta(fib2(N, F)).
(uncaught exception: error(permission_error
(modify,static_procedure,fib2/2),asserta/1))
fib2(1,1).
fib2(2,1).
fib2(N,F) :- fib3(N,F).
fib2(N,F) :- N>2,N1 is N-1,fib2(N1, F1),N2 is N2,fib2(N2,F2),F is F1+F2, asserta( fib3(N, F)).
57
8.5.6 Improving efficiency by asserting
derived facts
f(6)
+
f(4)
f(5)
3, retrieved
+
f(4)
f(3)
+
2, retrieved
f(3)
f(2)
+
1
f(2)
f(1)
1
1
58
8.5.6 Improving efficiency by asserting
derived facts






Asserting intermediate results, also called caching(是一種將先前讀進
來的資料留著,預備下一次讀取的技術), is a standard technique for
avoiding repeated computations.
It should be noted that we can preferably avoid repeated
computation by using another algorithm, rather than by asserting
intermediate results.
The other algorithm will lead to a program that is more diffcult to
understand, but more efficient to execute.
The idea is not to define the Nth Fibonacci number simply as the
sum of its two predecessors and leave the recursive calls to
unfold(展開) the whole computation ‘downwards’ to the two initial
Fibonacci numbers.
Instead, we can work ‘upwards’ starting with the initial two
numbers, and compute the numbers in the sequence one by one in
the forward direction.
We have to stop when we have computed the Nth number.
59
8.5.6 Improving efficiency by asserting
derived facts
NextF2

1
1
2
1
2
3
F1
F2
F
NextM
N
We can define a predicate
forwardfib( M, N, F1, F2, F)
Here, F1 and F2 are the (M-1)st and the Mth Fibonacci numbers,
and F is the Nth Fibonacci number.
fib3(N,F) :- forwardfib(2, N, 1, 1, F).
Tail-recursive call
forwardfib(M,N,F1, F2, F2) :- M >= N.
forwardfib(M,N,F1, F2, F) :- M < N, NextM is M+1,
NextF2 is F1 + F2, forwardfib( NextM, N, F2, NextF2, F).
60
8.5.6 Improving efficiency by asserting
derived facts
{trace}
| ?- fib3( 6, F).
| ?- fib3( 6, F).
F=8?
1 1 Call: fib3(6,_16) ?
yes
2 2 Call: forwardfib(2,6,1,1,_16) ?
3 3 Call: 2>=6 ?
3 3 Fail: 2>=6 ?
3 3 Call: 2<6 ?
3 3 Exit: 2<6 ?
4 3 Call: _140 is 2+1 ?
4 3 Exit: 3 is 2+1 ?
5 3 Call: _168 is 1+1 ?
5 3 Exit: 2 is 1+1 ?
6 3 Call: forwardfib(3,6,1,2,_16) ?
7 4 Call: 3>=6 ?
7 4 Fail: 3>=6 ?
fib3(N,F) :- forwardfib(2, N, 1, 1, F).
7 4 Call: 3<6 ?
forwardfib(M,N,F1, F2, F2) :- M >= N.
7 4 Exit: 3<6 ?
forwardfib(M,N,F1, F2, F) :- M < N,
NextM is M+1,
8 4 Call: _248 is 3+1 ?
NextF2 is F1 + F2,
8 4 Exit: 4 is 3+1 ?
forwardfib( NextM, N, F2, NextF2, F).
9 4 Call: _276 is 1+2 ?
9 4 Exit: 3 is 1+2 ? …
61
Exercise

Exercise 8.5

The following procedure computes the maximum value
in a list of numbers:
max([X],X).
max([X|Rest], Max) :max(Rest, MaxRest),
(MaxRest >= X,!, Max = MaxRest
;
Max = X).
Transform this into a tail-recursive procedure.
Hint: Introduce accumulator argument MaxSoFar.
62
Download