300 likes | 388 Views
Prolog Programming (Volume 5). Dr W.F. Clocksin. List cells + Unification = Great Idea. Normally we use ‘proper lists’, which are defined according to the theory of lists: A list of length 0 is [] A list of length N is [H|T] where T is a list of length N-1
E N D
Prolog Programming(Volume 5) Dr W.F. Clocksin
List cells + Unification = Great Idea Normally we use ‘proper lists’, which are defined according to the theory of lists: A list of length 0 is [] A list of length N is [H|T] where T is a list of length N-1 This is good when we only want to cons elements onto the from of the list. But, Prolog variables make it possible to do something else:
List cells + Unification = Great Idea zap([a, b, c, Z], Z, R) zap(X, d, X) so, R = [a, b, c, d] More generally, zap([a, b, c | Z], Z, R) zap(X, [d, e, f], X)
Difference Structures Probably one of the most ingenious programming techniques ever invented (by Sten-Åke Tärnlund, 1975) yet neglected by mainstream computer science. A way of using variables as ‘holes’ in data structures that are filled in by unification as the computation proceeds. Most examples here will be with difference lists. Will introduce by showing the usual (non difference list) algorithm for concatenating lists.
Concatenating two lists (WS 25) ?- append([a, b, c], [d, e, f], X). X = [a, b, c, d, e, f] append([], L, L). append([X|Y], T, [X|Z]) :- append(Y, T, Z). Notice this is tail recursive. This definition is possible because of logical variables. Definitions in other languages cannot be tail recursive because the last call is cons, not append.
Concatenating two lists (WS 25) What do these goals do? ?- append([a, b, c], X, [a, b, c, d, e, f]). ?- append([a, b, c], [d, e, f], X). ?- append(X, Y, [a, b, c, d, e, f]). X = [] Y = [a, b, c, d, e, f]; X = [a] Y = [b, c, d, e, f]; X = [a, b] Y = [c, d, e, f]; . . . . X = [a, b, c, d, e, f] Y = []
Linearising (flattening) a list (WS 25) Flattening a list is a way of listing the leaf nodes (finding the fringe) of tree-structured data. For example, [ a, b], [c, d, [e, f], g], h, [i, [k] ] ] flattens to [a, b, c, d, e, f, g, h, i, k] Here is the usual program: flatten([], []). flatten([H|T], L3) :- flatten(H, L1), flatten(T, L2), append(L1, L2, L3). flatten(X, [X]). This is very inefficient: how many calls to append?
Instead, Difference Lists The idea is to represent a list segment as a pair of terms, the front and the back. For example, the list segment L1-L2 has the elements a,b: L1 refers to the front of the list, and L2 refers to the back, often a variable. This variable can be used as a ‘hole’ into which another term may be instantiated. If the other term has a hole too, this can be useful. L1 L2 L1 [a, b | L2] a b
Example: Append L1-L2 to L3-L4 L1 L2 L3 L4 a d b e c Call the result X-Y. The following are true about X-Y: X should co-refer with L1 (to be the front of the list) Y should co-refer with L4 (to be the back of the list) L2 should co-refer with L3 (to join the lists together)
Make it so: L1 L2 L3 L4 X Y a d b e c In Prolog we can use this idea to implement constant-time appending of two lists. As suggested above, doing this is simply a matter of re-arranging variables.
Definition of app The goal app(L1,L2,L3,L4,X,Y) succeeds when the difference lists made from L1 and L2 and concatenated with the difference lists made from L3 and L4 to give the resulting difference list made from X and Y. app(L1, L2, L2, L4, L1, L4). Example execution: ?- app([a, b, c | Z1], Z1, [d, e, | Z2), Z2, X, Y). X = [a, b, c, d, e | Y]. A better way to see the arrangement of variables is like this: app(A, B, B, C, A, C).
Usual notation of difference lists Denote difference list with front L1 and back L2 as L1-L2, where - is an infix binary operator. This represents difference lists as a single term, so cuts down on the arity of procedures. Programs are clearer, as in this definition of app:. app(A-B, B-C, A-C). Example execution: ?- app([a, b, c | Z1]-Z1, [d, e, | Z2)-Z2, X-Y). X-Y = [a, b, c, d, e | Y]-Y. The extra space taken by the ‘-’ functor is not a serious problem in practice. But then, why use app when you can do the rearrangement of variables in-place where it is needed in a program!
Summary L-L is the null difference list [a | Z]-Z is the difference list containing ‘a’. Similarly, [a, b, c | Z]-Z is the difference list containing a, b, c. Unifying the difference list X with Y-[] will rectify the list, that is, turn it into a proper list. For example. unifying [a, b, c | Z]-Z with Y-[] instantiates Y to [a, b, c].
Linearising (flattening) a list (WS 28) flatten(X, Y) :- flatpair(X, Y-[]). flatpair([], L-L). flatpair([H|T], L1-L3) :- flatpair(H, L1-L2), flatpair(T, L2-L3). flatpair(X, [X|Z]-Z). Notice the pattern of ‘stitching’ elements together.
Linearising (flattening) Trees (WS 29) First, how to represent a binary tree. Leaves are any terms. Nodes are constructed from the term n(t1, t2) where terms t1 and t2 are trees called the left branch and the right branch. For example, the term n(n(a,b),n(c,n(d,e))) names the tree: n n n a b c n d e
Linearising (flattening) Trees (WS 29) Linearising a tree. This example will add interest by doing a partial map: only the integers will be linearised. The naive method (using append): lintree(n(A,B), L1) :- lintree(A, LA), lintree(B, LB), append(LA, LB, L1). lintree(I, [I]) :- integer(I). lintree(X, []).
Linearising (flattening) Trees (WS 29) The difference list method: lintree(n(A,B), L1) :- lintree(A, LA), lintree(B, LB), append(LA, LB, L1). lintree(I, [I]) :- integer(I). lintree(X, []).
Worked exercise for WS 29 /* picktree(tree, list of apples, list of pears) */ picktree(apple, [apple | L]-L, P-P). picktree(pear, A-A, [pear | L]-L). picktree(n(L,R), A1-A3, P1-P3) :- picktree(L, A1-A2, P1-P2), picktree(R, A2-A3, P2-P3). picktree(Other, A-A, P-P).
Normalising Sum Expressions (WS 30) + The sums (a+b)+(c+d) and (a+(b+(c+d))) may be normalised into a standard form that associates on the left: a+b+c+d, or equivalently, ((a+b)+c)+d. This is only possible because they are sums. + + d a c b + + a b + + c d + d c + b a
Define a predicate normsum The goal normsum(X,Y) succeeds when sum expression X normalises to Y. One method is to first flatten the sum, and then build up the normalised version in another pass. Here is flat(A,B,C), which flattens the sum into the difference list B-C: flat(X+Y), R1-R3) :- !, flat(X, R1-R2), flat(Y, R2-R3). flat(X, [X|Z] - Z). The difference ‘thread’ is R1R2R3, and note the ‘!’ to commit to the first clause when a sum node is encountered.
Building up the normalised sum This is not obvious. One way is to accumulate the ‘tree so far’, giving it a new parent node each time an element is found. This ‘stacks up’ nodes instead of inserting them. A ‘base case’ for nil is not needed because the flattened list will have at least two elements. build([X], T, T+X) :- !. build([H | L], T, Z) :- build(L, T+H, Z).
Putting them together Now normalise by flattening then building. The ‘tree so far’ in the second argument of build needs to be initialised with the first element of the flattened list: normalise(A, C) :- flat(A, B-[]), B = [T | L], build(L, T, C).
Do it in one pass instead! Here is a better program: normalise(X, Y) :- norm(X, []-Y). norm(X+Y, A-C) :- !, norm(X, A-B), norm(Y, B-C). norm(X, []-X) :- !. norm(X, A-(A+X)). Here the accumulator is used not only for differencing the elements, but also for building the tree so far. The constant [] is used to represent the null accumulator, but there is no list processing as such.
Find maximum element of a tree A valued (weighted, coloured) binary tree can be defined using compound terms. A node is represented by n(V,L,R), where V is the value (say a number), and L and R are the left and right branches of the tree. A terminal node will have L and R instantiated to []. For example the term: n(3, n(1, n(4, [], []), n(1, [], [])), n(5, n(9, n(2, [], []), []), n(6, [], n(5, [], [])))) represents the following tree:
3 1 5 6 9 1 4 2 5 Notice no terminals coming from here and here.
Find maximum element of a tree Just like finding the maximum element of a list, use an accumulator to represent the ‘biggest value so far’. /* findmax(Tree, Value so far, Final value) */ findmax([], A, A).findmax(n(V, L, R), A, Z) :- V >= A, !, findmax(L, V, A1), findmax(R, A1, Z).findmax(n(V, L, R), A, Z) :- V < A, !, findmax(L, A, A1), findmax(R, A1, Z).
Copy a tree Make a tree of the same shape as the input. /* copyTree(InputTree, OutputTree) */ copyTree([], []).copyTree(n(V, L, R), n(V, L1, R1) :- copyTree(L, L1), copyTree(R, R1).
Worksheet 32: Max Tree Given an input tree T, write a program that constructs a tree of the same shape as T, but in which the value of each node has been set to the value of the greatest node in T. This can be by finding the greatest element, and then making a copy inserting the greatest element as the value of each node in the copy. However, it can be done in one pass (!) as follows:
Worked Exercise /* mt(InTree, OutTree, Hole, AccumHighest, Highest) */ maxtree(A, B) :- mt(A, B, H, 0, H). mt(n(W, A, B), n(H, A1, B1), H, AC, N) :- W < AC, mt(A, A1, H, AC, ACA), mt(B, B1, H, ACA, N). mt(n(W, A, B), n(H, A1, B1), H, AC, N) :- W < AC, mt(A, A1, H, AC, ACA), mt(B, B1, H, ACA, N). mt([], [], H, A, A).
Difference Lists are... • Very efficient. • Popular -- in widespread use in Prolog. • Expressive of new methods of computation. • Sometimes misleading: difference-list append is not free of side-effects (but is backtrackable) • Often hidden. Programmers encapsulate the front and back in a structure p(L1, L2). Most common for this purpose is the infix ‘-’.