Difference Lists Explored
Difference Lists are a very powerful and useful tool. They're also difficult to understand and can result in difficult to read code. In this post we'll address both of these issues.
List vs. Difference List
In Prolog we don't have arrays, we have lists. Our lists are most efficiently accessed from the front, hence the syntax:
[Head|Tail]. Furthermore, the syntax for a list:
[a, b, c] is actually syntactic sugar for some list operator (traditionally
.(a, .(b, .(c, ))) .
A difference list is just a list with a variable at the end:
[a, b | C] or expanded
.(a, .(b, C)). This variable is often called the hole. These are not valid lists as they are not complete. By having a variable/hole at the end, we can unify this variable with a new end of the list so that we can now add elements to the end of the list. Unfortunately it doesn't allow us to pull elements off the end, only add new ones.
We'll also use
'·', so we can look at lists with the syntactic sugar and without. Run the following queries to see how we can unify the variables at the end of
DL with new values to eventually close the list.
Some common errors to watch out for:
- Not all list predicates can cope with difference lists. Warning: this query does not terminate, you can keep hitting "Next Answer" until the cows come home.
- A difference list closed without a list is not a valid list. Consider
.(a, .(b, c))vs
.(a, .(b, .(c, ))). Therefore a predicate expecting a complete, valid list will fail, as is shown with the query
length([a, b|c], L).
- Don't forget to use the
|operator, otherwise you end up with nested lists.
When writing code that is taking advantage of tail-call optimization we often use an accumulator list, however this method reverses the order of the list. By making this accumulator a difference list the order of the original can be preserved. We'll include an example at the end.
Writing Readable Difference Lists
For a whole host of reasons, code using difference lists can be difficult to read and difficult to spot:
- The empty difference list is a pair of variables:
dl(H, H), or just
H, and another floating
Hcould all be empty difference lists
- Difference list and hole pairs are syntax agnostic:
dl(L, H), or just
Hsomewhere else in the clause could all be non-empty difference lists
- New elements can be pushed to the difference list in head-of-rule unification, in the body, or in a distinct predicate
- Closing the difference list can be done with an empty list or a non-empty list
It's not that they're particularly difficult to understand conceptually, but the variety of ways they can be used means you really have to apply yourself to understanding how the author is using them when you're reading their code, once you've recognized they're using a difference list. It also gives you a plethora of choices when you decide to use one, making it difficult to remember the pattern of usage. If you do use one, make a note of it in your comments.
One solution? Write yourself a little library so you have uniformity in your own code. This is often the solution used for more complex data structures, such as queues and various types of trees. You'll find some Prolog dialects will provide libraries that handle these more complex structures for you, which reduces our own scope for introducing errors.
For my own library, I like to wrap up a difference list in it's own compound term, this way when I see
dl/2 I know what it is. There are a few things we need to be able to do:
- Create an empty difference list:
- Push an element onto the end of a difference list:
- Close a difference list:
- Create a difference list from a list, which in reverse will inefficiently close a difference list:
Let's code them.
Now we've abstracted away the not so pretty aspects of using difference lists we can use these predicates in application.
Applying Difference Lists
We'll run through a example of using a difference list, and contrast it against an example with a normal accumulator list, as well as one not using the above predicates. Use this to evaluate how you wish to use difference lists. The key trade-off to be wary of here is between readability and efficiency, you may find these additional predicates are more readable, but they're also slightly less efficient than unifying on the head of a rule. We change syntax from
dl/2 to the
List-Hole pair when not using the library predicates simply to demonstrate both.
How we use difference lists is really a trade off. The library of predicates make their use explicit and easy to reason about. For beginners they also abstract the mechanics of how they work away, again making them easier to use. However, this comes at an efficiency cost. In the above queries using the library required 21 resolution steps and took 6-7 milliseconds. Without the library this was 14 resolution steps and 3 milliseconds.
There's an old adage about not optimizing too early, which provides us with a good rule of thumb here. If efficiency isn't too critical or you're not comfortable with using difference lists yet, then enjoy the library knowing that when you need to speed things up, it's simple to do so. There's one golden rule though if you're using a difference list without a library, make it obvious in your comments!