harald@mulga.oz (Harald Sondergaard) (10/14/88)
Occasionally postings appearing in this newsgroup move to expel difference-lists from the Herbrand Elysium. It is not quite clear what this means, but the allegation seems to be that they do not qualify as terms, or are somehow second-class objects. Perhaps the reason for this discriminating attitude is that on the surface they promise to represent lists, but when we start unifying them, they are sometimes not faithful representatives. Unification of two difference-lists should correspond to unification of the lists they represent. However, there are cases where this is not so. Here are two examples that illustrate the problems: 1) The difference-lists nil\nil and a.nil\a.nil are not unifiable. But the lists they represent, nil and nil, are unifiable. 2) The difference-lists a.X\Y and Z\Z are unifiable. But the lists they represent, a.X and nil, are not unifiable. (Note that difficulties with difference-lists are often ascribed to the occur check problem, but the examples show that they are independent). In the light of the above, it is a bit of a miracle that the "folk" transformation from programs using lists to programs using difference-lists seems to work. In fact, sometimes it doesn't. Consider the program double(X,Y) :- append(X,X,Y). (*) The transformation yields double(X,Y) :- double'(X\nil,Y\nil). double'(X\X',Y\Y') :- app(X\X',X\X',Y\Y'). app(X\Y,Y\Z,X\Z). Unfolding the calls to double' and app, we get the program double(nil,nil). This program is rather different to (*). This does not mean that difference-lists are second-class objects. It just means that we must be careful using them in the "folk" transformation. A discussion of the problems with using difference-lists and how they can be avoided by dataflow analysis can be found in: Marriott, K. & H. Sondergaard, Prolog program transformation by introduction of difference-lists, Proc. Int. Computer Science Conf. '88, Hong Kong (1988) to appear. Preliminary version as TR 88/14, Dept. of Computer Science, Univ. of Melbourne (1988). --- Kim Marriott & Harald Sondergaard
ok@quintus.uucp (Richard A. O'Keefe) (10/15/88)
In article <3002@mulga.oz> harald@mulga.oz (Harald Sondergaard) writes:
[a lot of sensible things about difference lists]
I would like to suggest that most of the trouble comes from trying
to form a single data structure Front\Back and think of this as a
list in its own right. If instead you think in terms of
"difference PAIRS" -- that is, pairs of arguments to a relation
which makes a statement about the difference between them -- you
run into less trouble (or at any rate I do).
For example, suppose we want to express
double(X, Y) :- append(X, X, Y)
in terms of differences. I would _think_ and write
% double(X0,X, Y0,Y)
% is true when Y0\Y = (X0\X)<>(X0\X)
double(X0,X, Y0,Y) :-
append(DX, X, X0),
append(DX, Y1, Y0),
append(DX, Y, Y1).
and would start my transformation from that, arriving at
double(X0, X, Y0, Y) :-
double(X0, X, Y0, Y1, Y1, Y).
double(X, X, Y, Y, Z, Z).
double([E|X1], X, [E|Y1], Y, [E|Z1], Z) :-
double(X1, X, Y1, Y, Z1, Z).
without any false steps.
Using difference pairs rather than "difference lists" also tends to
result in more efficient programs, as you aren't all the time unpacking
Front\Back terms and building new ones.
harald@mulga.oz (Harald Sondergaard) (10/17/88)
In article <542@quintus.UUCP>, ok@quintus.uucp (Richard A. O'Keefe) writes: > I would like to suggest that most of the trouble comes from trying > to form a single data structure Front\Back and think of this as a > list in its own right. If instead you think in terms of > "difference PAIRS" -- that is, pairs of arguments to a relation > which makes a statement about the difference between them -- you > run into less trouble (or at any rate I do). We agree that the use of "difference pairs" is preferable, but they do not really solve our problem. Our concern is with automatic, correct transformation of programs using lists to programs using difference-lists (or difference pairs - it makes no difference). There seem to be two approaches to transformation using difference-lists, "folk" and "unfold" (no, no misprints there :-). The former is exemplified in Sterling and Shapiro's book, the latter in a recent ECAI paper by Zhang and Grant. The example transformation suggested by Richard seems to be of the "unfold" school. He transforms > double(X0,X, Y0,Y) :- > append(DX, X, X0), > append(DX, Y1, Y0), > append(DX, Y, Y1). into > double(X0, X, Y0, Y) :- > double(X0, X, Y0, Y1, Y1, Y). > > double(X, X, Y, Y, Z, Z). > double([E|X1], X, [E|Y1], Y, [E|Z1], Z) :- > double(X1, X, Y1, Y, Z1, Z). which seems to follow the unfold-eureka-fold pattern, the "eureka" part being the definition of double/6. Unfortunately, folding is not a safe transformation in general, as it may introduce or eliminate infinite computations. Indeed, this happens in Richard's example. Whereas with the first definition of double, ?- double(X0,X,nil,nil) does not terminate on backtracking (after having returned an answer), with the second definition it does. It is interesting to note that for similar reasons, the first definition of double/4 is not "equivalent" to the original definition we gave of double/2: double(X,Y) :- append(X,X,Y). We became interested in the "folk" variant because of the following observations: (1) Even in the presence of occur checks, unification of difference-lists does not mimic unification of lists faithfully. (2) Independently of (1), the "rapid append" used for difference-lists is not semantically equivalent to the usual append. (3) In spite of (1) and (2), the "folk" transformation seems to work well for the majority of useful list- processing programs, "double" being an exception. (4) Moreover, the "folk" transformation is so simple that it can readily be made automatic. We conclude that correctness is a problem for both schools. However, an additional problem with the "unfold" approach is the "eureka" step, which makes it harder to automate. Note that difference pairs could just as well be used in the "folk" variant (Sterling and Shapiro mention this themselves). However, (1) - (4) would still apply, in particular the conditional safeness. It therefore seems that representation is not the crux of the matter. One approach to automating the transformation of programs that use lists to programs that use difference-lists (or difference pairs) is to find conditions under which the "folk" transformation is safe. Dataflow analysis can then be used to determine whether the transformation is applicable to a given program. This is the approach taken in our paper. -- Kim Marriott & Harald Sondergaard
lee@munnari.oz (Lee Naish) (10/18/88)
In article <3005@mulga.oz> harald@mulga.oz (Harald Sondergaard) writes: > There seem to be two approaches to transformation using >difference-lists, "folk" and "unfold" (no, no misprints there :-). >The former is exemplified in Sterling and Shapiro's book, the latter >in a recent ECAI paper by Zhang and Grant. > The example transformation suggested by Richard seems to be of >the "unfold" school. I have not seen the paper by Zhang and Grant, but I am also of the "unfold" school (I have tried to convert Kim and Harald without success, so far at least :-). Fold/unfold transformations are fairly well understood and provide a good theoretical framework. I think it can be automated quite easily also, as long as the "eureka" steps are known. For the list-> difference list/pair transformation, the first trick (at least to my mind), is to spot occurrences of .... p(...X...), append(X, Y, Z).... where X doesn't appear anywhere else and "fold" this into ....p'(...Y, Z...).... The definition of p' can be obtained from the definitions of p and append. Often this definition can be simplified, using the transitivity of append (another "eureka"), then folded to get a recursive p' predicate which uses difference pairs rather than simple lists. This last step is the problem as far as preserving semantics is concerned. The two versions have the same least model but in the transformed version generally makes p' true in more models of (the completion of) the program. It is a more complex version of replacing p:-fail by p:-p. This causes infinite loops. Goals which where false in all models in the original version (and hence finitely failed if the computation rule was fair) are true in some but not all models in the transformed version (so they loop). Here is what we get with reverse: % original, after simplifying append(E, [A], F), append(F, C, D) reverse'([], A, A). reverse'(A.B, C, D) :- reverse(B, E), append(E, A.C, D). % folded version reverse'([], A, A). reverse'(A.B, C, D) :- reverse'(B, A.C, D). The first version finitely fails with the goal reverse'(X, [a], [b]), assuming a fair computation rule. The second version loops with the same goal. The reason is that there is a (non-Herbrand) model in which an instance of the goal is true. This also explains why O(N) reverse causes loops when run backwards. I (with Kim and Harald) looked for a transformation which preserves all models without much luck. If you use the trick of keeping the "length" of the difference list around it works for some cases (for reverse you get something like the version which calls same_length and can run backwards). For tree traversals, the benefit of difference lists was lost however (the algorithm was O(N^2) due to manipulation of lengths). I tried using integers and "successor" notation and neither seemed to work. Any ideas, anyone? Also if anyone would like to verify my claim that the d-list "fold" style transformation can be automated, I would encourage it (are you listening Richard? :-). lee
ok@quintus.uucp (Richard A. O'Keefe) (10/19/88)
In article <2502@munnari.oz> lee@munmurra.UUCP (Lee Naish) writes: >Also if anyone would like >to verify my claim that the d-list "fold" style transformation can be >automated, I would encourage it (are you listening Richard? :-). I am listening. I am interested. But I am also _busy_. Just one point about the "unfold" approach: the Eureka steps aren't really all that hard to find. Basically, you are looking for a clump of goals which look as though they can be unfolded together, and you usually have to generalise the clump a little bit to stop the unfold steps getting in each other's way.
gerdeman@clio.las.uiuc.edu (10/20/88)
Did something go wrong in the Eureka step here? What does the variable X do? double(X0, X, Y0, Y) :- double(X0, X, Y0, Y1, Y1, Y). double(X, X, Y, Y, Z, Z). double([E|X1], X, [E|Y1], Y, [E|Z1], Z) :- double(X1, X, Y1, Y, Z1, Z). Why not write ; double(X0,Y0,Y):- double(X0,Y0,Y1,Y1,Y). double([],Y,Y,Z,Z). double([E|X1],[E|Y1],Y,[E|Z1],Z):- double(X1,Y1,Y,Z1,Z).
ok@quintus.uucp (Richard A. O'Keefe) (10/21/88)
In article <16400002@clio> gerdeman@clio.las.uiuc.edu writes: >Did something go wrong in the Eureka step here? NO. >What does the variable X do? It is called for by the original specification. Namely, the INPUT of double/4 is a difference pair as well as the OUTPUT. > double(X0, X, Y0, Y) :- > double(X0, X, Y0, Y1, Y1, Y). The "input" is the "virtual" list X0\X, just as the "output" is the "virtual" list Y0\Y. > double(X, X, Y, Y, Z, Z). Testing whether there is nothing between the first two arguments is the equivalent of testing whether the "virtual" input is empty. >Why not write ; > double(X0,Y0,Y):- > double(X0,Y0,Y1,Y1,Y). Because it wouldn't meet the original specification, that's why not.
johnson@csli.STANFORD.EDU (Mark Johnson) (10/24/88)
Given all the interest in Unfold/Fold, would anyone care to hear about a toy program I wrote that uses the Unfold/Fold transformation on the following input clauses to derive the following output? If so, I will write a short description for the net. Mark Johnson Preferred return address: johnson@csc.brown.edu Input clauses (assertions in database). string(n(X,Y),S) <- [ string(X,SX), string(Y,SY), append(SX,SY,S) ]. string(X,[X]) <- [terminal(X)]. terminal(a) <- []. terminal(b) <- []. revpairwise([],[]) <- []. revpairwise([A],[A]) <- []. revpairwise([A,B|Es],Rs) <- [revpairwise(Es,Ms), append(Ms,[A,B],Rs)]. append([],Cs,Cs) <- []. append([A|As],Bs,[A|Cs]) <- [ append(As,Bs,Cs) ]. Sample Interaction. fold(string(_,_)). % string_f(A,B,C) :- % string(A,D), % append(D,B,C). string_f(n(A,B),C,D) :- string_f(A,E,D), string_f(B,C,E). string_f(A,B,[A | B]) :- terminal(A). No fold(revpairwise(_,_)). % revpairwise_f(A,B,C) :- % revpairwise(A,D), % append(D,B,C). revpairwise_f([],A,A). revpairwise_f([A],B,[A | B]). revpairwise_f([A,B | C],D,E) :- revpairwise_f(C,[A,B | D],E). No
johnson@csli.STANFORD.EDU (Mark Johnson) (10/27/88)
I've received a couple of messages expressing interest in my toy unfold/folder, so here's a longer description. Here's a description of my little program: please remember that it is only a first attempt at applying the Unfold/Fold transformation automatically, and that as I note at the bottom of the article, I think it can be improved considerably. Even so, it is capable of performing the following Unfold/Fold transformations on programs like (naive) reverse, the program "string" shown below, and other "simple" recursive programs to yield their difference list counterparts. string(n(X,Y),S) :- string(X,SX), string(Y,SY), append(SX,SY,S). string(X,[X]) :- terminal(X). terminal(a). terminal(b). terminal(c). It consists of 3 major predicates. To make things simpler, I describe each predicates' application to naive reverse as a running example: Example Input program (as assertions in data base). reverse([],[]) <- []. reverse([E|Es],Rs) <- [ reverse(Es,Ms), append(Ms,[E],Rs) ]. 1. foldDefn(GoalToFold,FoldDef) This proposes a definition clause FoldDef for the folding process based on the clauses for GoalToFold. FoldDef is currently computed by examining the recursive clauses for GoalToFold. foldDefn(reverse(_,_),FoldDef). FoldDef = reverse_f(_2,_820,_8)<-[reverse(_2,_14),append(_14,_820,_8)] ; FoldDef = reverse_f(_2,_0,_8)<-[reverse(_2,_14),append(_14,[_0],_8)] ; FoldDef = reverse_f(_2,_0,_830,_8)<-[reverse(_2,_14),append(_14,[_0 | _830],_8)] ; 2. unfoldFoldDefn(FoldDef,GoalToFold,Unfoldeds) This unfolds all instances of the GoalToFold in FoldDef to yield the unfolded clauses. It also performs some elementary simplifications on the result. unfoldFoldDefn(reverse_f(_2,_820,_8)<-[reverse(_2,_14),append(_14,_820,_8)], reverse(_,_),Unfoldeds). Unfoldeds = [ reverse_f([],_4,_4)<-[], reverse_f([_62 | _64],_4,_6)<- [reverse(_64,_74), append(_74,[_62],_12), append(_12,_4,_6)]] ; 3. fold(Unfolded,Folders,Folded) This folds the body of Unfolded by the bodies of Folders to yield Folded. Folders includes equivalences which may be useful in the folding process as well as FoldDef itself. In our example, note that there is nothing to fold in the first clause of Unfoldeds. Note the equivalences involving append that are given to the folder: only the first of these is used here, the second is required when folding the unfolded body of the FoldDef for the "string" predicate defined above. fold(reverse_f([_62 | _64],_4,_6)<- [reverse(_64,_74),append(_74,[_62],_12),append(_12,_4,_6)], [ [reverse_f(A1,B1,C1)]<-[reverse(A1,D1),append(D1,B1,C1)], [append(X,[A|V],W)] <- [ append(X,[A],Y), append(Y, V, W) ], [append(B,D,X),append(A,X,E)] <- [append(A,B,C),append(C,D,E)] ], Folded). Folded = reverse_f([_0 | _2],_8,_10)<-[append(_28,_8,_10),reverse_f(_2,[_0],_28)] ; Folded = reverse_f([_0 | _2],_8,_10)<-[reverse_f(_2,[_0 | _8],_10)] ; Folded = reverse_f([_0 | _2],_8,_10)<-[append(_16,[_0 | _8],_10),reverse(_2,_16)] ; Folded = reverse_f([_0 | _2],_8,_10)<-[reverse_f(_2,_254,_10),append([_0],_8,_254)] ; Folded = reverse_f([_0 | _2],_8,_10)<- [append(_16,_254,_10),append([_0],_8,_254),reverse(_2,_16)] ; Folded = reverse_f([_0 | _2],_8,_10)<- [append(_28,_8,_10),append(_16,[_0],_28),reverse(_2,_16)] ; The second of these answers is the preferred one, because it does not contain either the original program predicate we are trying to redefine, or the predicate "append". The program might be improved in the following ways. First, step 1, ie. choosing a FoldDef, should incorporate the constraint that the FoldDef actually be capable of computing the relation to be folded! This would rule out all but the first proposed FoldDef in step 1 of the example above, and more importantly, may prove useful as a way of generating possible FoldDefs. Second, the unfolding step (step 2) could be modified so that it doesn't just do a single unfold of the FoldDef head, but instead unfolds all goals that contain a "recursive argument" of the FoldDef. With this extension, the procedure may be able to Unfold/Fold programs that are more complicated than the simple "one-level" recursive ones that it can deal with now.