Understanding Difference Lists
Asked Answered
T

2

21

I'm trying to understand difference lists in Prolog, but I'm struggling to actually implement one properly, everytime I try to do it, I get a list of lists, but that's not what I want. I'm trying to implement an append predicate, but having little luck so far. Few attempts, all of which don't work.

app(X, Y, Z) :- Z = [X|Y].

?- app([a,b,c], [z], Z).
Z = [[a,b,c],z].

OR

app(X, Y, Z) :- Z = [X|Hole], Hole = Y.

Same results as the first one, (they do seem to be basically the same). I have an example in a book that does work (although it's not a predicate), and I don't understand the difference. X becomes instantiated to the proper answer [a,b,c,z], how is that much different than my second example?

X = [a,b,c|Y], Y = [z].

What am I missing? Thanks.

Thornberry answered 17/11, 2014 at 5:22 Comment(4)
it's the old, tricky business of 'trading space vs time'Stuff
Difference lists are just the simpler of so called 'incomplete data structures'. So basics in Prolog... You should not try to implement them by yourself (apart studying what's going on, of course), but study DGC instead, that is, applied difference lists. See Markus page for an introductionStuff
@Stuff I agree that DCGs are definitely better in almost all cases. However, at least SWI-Prolog has a few built in predicates that work with difference lists (because they have to: for example, read_pending_input/3), so, unfortunately, difference lists cannot be fully avoided, at least as long there is no complete implementation of pure input/output.Homesteader
Of interest: SWI-Prolog Discourse forum - Wiki: Difference ListShatter
H
34

The key to understanding difference lists is understanding what they are on the level of the nested compound term that represents lists. Normally, we see a list like that:

[a, b, c]

This is now a list with three elements. The exactly same nested term using the dot as the list functor, ./2, and the atom [] as the empty list, would be:

.(a, .(b, .(c, [])))

It is important here that the list functor is a compound term with two arguments: the element and the rest of the list. The empty list is an atom, which, informally, could be seen as a compound term with arity 0, that is, without arguments.

Now, this is a list with three elements where the last element is a free variable:

[a, b, Last]

which is the same as:

.(a, .(b, .(Last, [])))

This, on the other hand, is a list with two elements and a free variable as the rest of the list, or the tail:

[a, b|Tail]

which is the same as:

.(a, .(b, Tail))

Do you see how .(a, .(b, .(Last, []))) is not the same as .(a, .(b, Tail))?

Trying this on from the top-level (I am using SWI-Prolog 7, which needs the --traditional flag to treat the ./2 as the list term):

$ swipl --traditional
Welcome to SWI-Prolog (Multi-threaded, 64 bits, Version 7.1.26)
Copyright (c) 1990-2014 University of Amsterdam, VU Amsterdam
SWI-Prolog comes with ABSOLUTELY NO WARRANTY. This is free software,
and you are welcome to redistribute it under certain conditions.
Please visit http://www.swi-prolog.org for details.

For help, use ?- help(Topic). or ?- apropos(Word).

?- [a, b, Last] = [a, b|Tail].
Tail = [Last].

?- .(a, .(b, .(Last, []))) = .(a, .(b, Tail)).
Tail = [Last].

Now, a "difference list" is a list like this: [a, b|Tail], identical to .(a, .(b, Tail)), where you hold on the variable Tail that holds the tail. This is not a proper list until the Tail has been instantiated to a proper list!

?- L = [a, b|Tail], is_list(L).
false.

?- L = [a, b|Tail], Tail = [c,d,e], is_list(L).
L = [a, b, c, d, e],
Tail = [c, d, e].

You can look at the previous queries to understand what exactly Tail = [c, d, e] does in this conjunction.

In a predicate that uses a difference list, you need two arguments, or sometimes, a pair, to hold on to the incomplete list and its tail, like this:

% using two arguments
foo([a,b|Tail], Tail).
% using a pair
foo([a,b|Tail]-Tail).

The first foo/2 has two arguments, the second has one, which is a "pair". Modern Prolog code seems to prefer two arguments to a pair, but you see the pair in textbooks and tutorials quite often.

To your append, or app/3: When you are working with difference lists, you need the extra argument (or a pair) so that you can access the tail of the list you are dealing with. If you only have the tail of the list that will be at the front, you can still write an append that only has three arguments, because all it takes is to unify the tail of the first list with the second list:

% app(List1, Tail1, List2)
app(List1, Tail1, List2) :- Tail1 = List2.

or unifying directly in the head:

app(_L1, L2, L2).

?- L1 = [a,b|Tail], app(L1, Tail, [c]).
L1 = [a, b, c],
Tail = [c].

This is the exact same code as in the link provided by @Wouter.

If you had the tails of both lists, you will replace the tail of the first list with the second list, and keep the tail of the second list.

app(List1, Tail1, List2, Tail2) :- Tail1 = List2.

Again, you could have done the unification in the head.

EDIT:

You can't make a "hole" once the list is already fully instantiated. How would you go from this .(a, .(b, .(c, []))) to this: .(a, .(b, .(c, Tail)))? You can't, except for traversting the list head to end and replacing the [] with Tail, but this is exactly what the ordinary append/3 does. Try:

?- L = [a,b,c,d], append(L, Back, Front), Back = [x,y,z].
L = [a, b, c, d],
Back = [x, y, z],
Front = [a, b, c, d, x, y, z].

Or, if you have a diflist_append/3 defined as:

diflist_append(Front, Back, Back).

Which unifies the Back of list with the third argument:

?- L = [a,b,c,d], append(L, Back, Front), diflist_append(Front, Back, [x,y,z]).
L = [a, b, c, d],
Back = [x, y, z],
Front = [a, b, c, d, x, y, z].

As for your example, X = [a,b,c], Y = [X|Z], Z = [z], this is the same as:

X = .(a, .(b, .(c, []))),
Y = .(X, Z), % Y = .(.(a, .(b, .(c, []))), Z)
Z = [z] % Y = .(.(a, .(b, .(c, []))), .(z, []))

So do you see it now?

Homesteader answered 17/11, 2014 at 7:34 Comment(7)
I guess I see why when I do something like app(X, Y, Z) :- Z = [X|Hole], Hole = Y., is not working. Because I'm trying to use the original list as a head, but not as one list with what's left over. But is there a way to utilize a difference list predicate, without the user having to pass in a difference list? Like if you were making an API or something?Thornberry
But to be honest, I am having some difficulty with why X = [a,b,c|Y], Y = [z]. is different than X = [a,b,c], Y = [X|Z], Z = [z].. Thanks again.Thornberry
@Thornberry If you wrote down what these unifications would look like when you used the ./2 nested term instead of lists you would see it immediately. Also see the edit.Homesteader
Thanks that's helpful, I understand it now. I was seeking to use difference lists for append/3 without passing one in, but I can see that that's not possible now.Thornberry
the predicate, write_canonical(List), can be used to see what the unifications look like in the ./2 formatMicrogroove
@Microgroove Not with SWI-Prolog 7. See here for information.Homesteader
@Microgroove You can use write_term(List, [dotlists(true)]) instead.Homesteader
G
8

Paul Brna has explained this very well. He uses variables OpenList# and Hole# in his difference-list version of append:

difference_append(OpenList1-Hole1, Hole1-Hole2, OpenList1-Hole2).

Example of use:

?- difference_append([a,b,c|H1]-H1, [d,e,f|H2]-H2, L).
H1 = [d, e, f|H2],
L = [a, b, c, d, e, f|H2]-H2.
Guienne answered 17/11, 2014 at 6:11 Comment(2)
Thanks for the link, I had read it before, but am able to understand what's actually going on better now.Thornberry
Paul Brna's course notes are available as a PDF here: cs.fsu.edu/~cap5605/brna/bk.pdf. Go to section 12.2 "Open Lists and Difference Lists" for the content referenced above.Testicle

© 2022 - 2024 — McMap. All rights reserved.