Lab 6
Lab 6
1. Append
We shall define an important predicate append/3 whose arguments are all lists.
Viewed declaratively, append(L1,L2,L3) will hold when the list L3 is the
result of concatenating the lists L1 and L2 together. For example, if we pose
the query
?- append([a,b,c],[1,2,3],[a,b,c,1,2,3]).
we will get the response ‘yes’. On the other hand, if we pose the query
?- append([a,b,c],[1,2,3],[a,b,c,1,2]).
we will get the answer ‘no’.
1
If we carried out a trace on what happens next, we would get something like the
following:
append([a, b, c], [1, 2, 3], _G518)
append([b, c], [1, 2, 3], _G587)
append([c], [1, 2, 3], _G590)
append([], [1, 2, 3], _G593)
append([], [1, 2, 3], [1, 2, 3])
append([c], [1, 2, 3], [c, 1, 2, 3])
append([b, c], [1, 2, 3], [b, c, 1, 2, 3])
append([a, b, c], [1, 2, 3], [a, b, c, 1, 2, 3])
X = [a, b, c, 1, 2, 3]
yes
The basic pattern should be clear: in the first four lines we see that Prolog
recurses its way down the list in its first argument until it can apply the base case
of the recursive definition. Then, as the next four lines show, it then stepwise ‘fills
in’ the result. This ‘filling in’ process is carried out by successively instantiating
the variables _G593, _G590, _G587, and _G518. Try to relate to the search tree
given below:
append([a,b,c],[1,2,3],_G518)
_G518 = [a|_G587]
append([b,c],[1,2,3],_G587)
_G587 = [b|_G590]
append([c],[1,2,3],_G590)
_G590 = [c|_G593]
append([],[1,2,3],_G593)
_G593 = []
2
Work through this example carefully, and make sure you fully understand the
pattern of variable
instantiations, namely:
_G518 = [a|_G587] = [a|[b|_G590]] = [a|[b|[c|_G593]]]
One important use of append is to split up a list into two consecutive lists. For
example:
append(X,Y,[a,b,c,d]).
X = []
Y = [a,b,c,d] ;
X = [a]
Y = [b,c,d] ;
X = [a,b]
Y = [c,d] ;
X = [a,b,c]
Y = [d] ;
X = [a,b,c,d]
Y = [] ;
no
Thus, we can define a program which finds prefixes of lists. For example, the
prefixes of [a,b,c,d] are [], [a], [a,b], [a,b,c], and [a,b,c,d]. With
the help of append it is straightforward to define a program prefix/2, whose
arguments are both lists, such that prefix(P,L) will hold when P is a prefix of
L. Here’s how:
prefix(P,L) :- append(P,_,L).
This predicate successfully finds prefixes of lists, and moreover, via backtracking,
finds them all:
prefix(X,[a,b,c,d]).
X = [] ;
X = [a] ;
X = [a,b] ;
3
X = [a,b,c] ;
X = [a,b,c,d] ;
no
In a similar fashion, we can define a program which finds suffixes of lists. For
example, the suffixes of [a,b,c,d] are [], [d], [c,d], [b,c,d], and
[a,b,c,d]. Again, using append it is easy to define suffix/2, a predicate
whose arguments are both lists, such that suffix(S,L) will hold when S is a
suffix of L:
suffix(S,L) :- append(_,S,L).
This predicate successfully finds suffixes of lists, and moreover, via backtracking,
finds them all:
suffix(X,[a,b,c,d]).
X = [a,b,c,d] ;
X = [b,c,d] ;
X = [c,d] ;
X = [d] ;
X = [] ;
no
Now it’s very easy to define a program that finds sublists of lists. The sublists of
[a,b,c,d] are [], [a], [b], [c], [d], [a,b], [b,c], [c,d], [d,e],
[a,b,c], [b,c,d], and [a,b,c,d]. A little thought reveals that the sublists of
a list L are simply the prefixes of suffixes of L. Pictorially representing:
4
That is, SubL is a sublist of L if there is some suffix S of L of which SubL is a
prefix. This program doesn’t explicitly use append, but under the surface, that’s
what’s doing the work for us, as both prefix and suffix are defined using
append.
2. Reversing Lists
Append is a useful predicate but it can be a source of inefficiency, and that you
don’t want to use it all the time.
The source of its inefficiency is: append doesn’t join two lists in one simple
action. Rather, it needs to work its way down its first argument until it finds the
end of the list, and only then can it carry out the concatenation.
If we have two lists and we just want to concatenate them, this is probably not
too problematic. But matters may be very different if the first two arguments are
given as variables. As we’ve just seen, it can be very useful to give append
variables in its first two arguments, for this lets Prolog search for ways of splitting
up the lists. But there is a price to pay: a lot of search is going on, and this can
lead to very inefficient programs.
With the help of append it is easy to turn this recursive definition into Prolog:
naiverev([],[]).
naiverev([H|T],R) :- naiverev(T,RevT),append(RevT,[H],R).
Try to trace a query for this predicate. The trace is very instructive with the
program spending a lot of calling append. The result is thus, very inefficient.
There is a better way of reversing lists.
5
2.2 Reverse using accumulator
The better way is to use an accumulator. The accumulator will be a list, and
when we start it will be empty.
The recursive clause is responsible for chopping of the head of the input list,
and pushing it onto the accumulator. The base case halts the program, and
copies the accumulator to the final argument.
It’s a good idea to write a predicate which carries out the required
initialization of the accumulator for us:
rev(L,R) :- accRev(L,[],R).
Try to trace this predicate after posing a query. It is clearly better than naiverev
as it is much less instructive.
3. Exercises
6
Write a predicate doubled(List) which succeeds when List is a
doubled list.
7
Hint: here’s where append comes in useful.
11. We ‘flatten’ a list by removing all the square brackets around any
lists it contains as elements, and around any lists that its elements contain
as element, and so on for all nested lists. For example, when we flatten the
list [a,b,[c,d],[[1,2]],foo] we get the list
8
[a,b,c,d,1,2,foo] and when we flatten the list
[a,b,[[[[[[[c,d]]]]]]],[[1,2]],foo,[]]
we also get [a,b,c,d,1,2,foo].
Write a predicate flatten(List,Flat) that holds when the first
argument List flattens to the second argument Flat. This exercise can be
done without making use of append.