The second day with Prolog is about the main data structure (the list) and the writing of rules.
Lists in Prolog are just the same as in Lisp: either empty, or a pair with the head (a single element) and the rest of the list:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
Rules are superficially similar to the definition of functions in functional languages (especially Erlang), but the similarity is treacherous. In both, rules or functions can have multiple definitions, each with different patterns; in both, the pattern must match for the rules to fire or the function to be executed; but in functional languages, only the body of the first matching pattern will be executed, whereas in Prolog all the matching patterns can fire (there are ways to control that, but they are not covered in this book).
Another way in which they differ is that they do not return a value: whatever they return must be unified against one of the parameters. While this might appear clumsy, it has a significant benefit: rules can relate parameters in more than one direction (see the section ‘Using Rules in Both Directions’ in the book). Consider this:
1 2 3 4 5 6 7 8 9 10
The first query is a typical invocation; the second ask what a list of length 3 might look like. Rules do not always support such multiple interpretations, and it is not always easy to know which does. But writing such rules is like writing functions that handle infinite data structure in Haskell: a necessary step on the way from superficial knowledge of the language to deeper understanding.
The second answer is a list of 3, unbound variables. Combined with other rules,
Sometimes the alternative interpretation is unbounded. Consider
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
The first query will match for each element (3 in total), as
X is unconstrained. But the second query tries to find what a list that contains 1 look like; obviously there is an infinite number of such list (so I aborted the query after the 8 first answers).
I could constrain the list in different ways, for instance:
1 2 3 4 5 6 7 8 9
Or more interestingly, querying the sublists of length 3 of another list:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
Now, if you have read the book already past Prolog Day 3, think about what the kind of code above could have done to reduce the list of variables.
Prolog would be a very limited language without a way to build queries from individual components. Fortunately, it comes with a number of predicates that let you do just that.
call is one such predicates, and it is fairly powerful.
The most basic invocation just specifies the target predicate, then the arguments, each as a separate argument to
1 2 3 4 5
Passing the predicate as a atom via a variable is also supported:
1 2 3 4 5 6
The actual predicate could come from even stranger places:
1 2 3 4 5 6
call supports a kind of currying (partial application): the first argument can specify it’s first parameters:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
P is unified with two partially applied predicates,
member(y). Then each is applied to a list of length 2. Note that backtracking works across
As a side note: a predicate followed by a list of arguments (either variables or atoms or other valid Prolog values) is called a structure. It is a kind of names tuple. In a Prolog file, it can be used to define facts. At the Prolog prompt, it can be used to run queries. But it can also be used as generic data structure, or as argument to
call and similar functions.
After the preamble above, the exercises won’t be too taxing.
This was not exactly an exercise, but the implementation is really simple. It doesn’t take too long to come up with a recursive algorithm (and for those who really can’t, check the Wikipedia page):
- to move one disk from a peg to another, just do it;
- to move a stack of disks from one peg to a second one, first move all but the last disk to the third peg, move the last disk to the second peg, then move all the disks previously moved from third to second.
A translation in Prolog is equally simple:
1 2 3 4 5 6 7 8 9 10
It should be easy to match the algorithm to the implementation.
1 2 3 4 5 6 7 8 9 10 11 12
Prolog cannot handle not expressions properly. Fundamentally, all it can say is whether a query can be proven to true. If it says no, it just means it could not be proven true, which is slightly different from being proven false.
Because Prolog is a Turing complete language, it could also fail to return an answer to a specific query, meaning either the query is false, or it just needs a bit more time to be proven true…
There are newer languages (Coq for instance) that were designed as not Turing complete (which makes them interesting. Anybody can invent a Turing complete language. It takes far more work to come up with a useful language that isn’t complete), and can handle not expressions over larger logic domains.
Reverse elements in a list
I am using an accumulator to build the reverse. Note that in Prolog, predicates have a specific number of arguments called the predicate arity. Predicates with the same name but different arity are different predicates. In the solution below, I have two predicates:
1 2 3 4
Let’s ignore the first line for a moment.
my_reverse/3, passing an empty accumulator.
my_reverse/3 just iterates over the element of the list, adding them to the accumulator. When it runs out of element, the accumulator becomes the result.
First, the code works for at least the basic case:
1 2 3 4 5
More interestingly, the code also works when the parameters are both variables, or partially defined lists:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
The first query just states that both lists are reverse of each other, but does not mention any content. There is an infinite number of such list, and the code dutifully return them in order; the fresh variables used as elements for the first list are indeed in reverse order in the second.
The second query specifies a prefix, but not the end of the first parameter. Once again, there is an infinite number of possible answers, a few of which are shown above.
Now back to the first line in the definition: the code checks if the second parameter is
compound, in other word if it is at least partially defined. This is to make the following query work:
1 2 3 4 5
Without this line, the first result would be returned, but then Prolog would hang looking for a second (non existing) result. I must admit I do not fully understand why (rules with multiple interpretations are complex to design). I also make sure this rule is the only one to match but using the cut
! operator: once the rule has started to match, any backtracking that could have happened in this rule is cut. In other words, the second pattern will not match, even though it could.
Backtracking introduced by previous rules is still available:
1 2 3 4 5 6 7 8 9 10
member clause can backtrack over the two elements; the cut in
my_reverse does not prevent it.
The smallest element in a list
This time the code is not fancy at all. Using arithmetic operators (such as min) tends to constraint the code in a way that prevent fancy use (as in
my_reverse above). So
my_min does what it needs to do as simply as possible:
Either there’s only one element in the list, and this is the minimum, or the minimum of a list is the minimum between the first element of the list, and the minimum of the rest of the list.
Sort the elements of a list
For this exercise, I will use the insert sort. Slow but easy. Faster implementations are provided as standard predicates in decent Prologs anyway.
1 2 3 4 5 6
Once again, nothing fancy: the empty list is already sorted; to sort a larger list, sort the tail of the list, then insert the head at the right position.
To insert an element in a sorted list, if the list is empty, then the singleton list with that element is the answer. Otherwise, compare the element to the head: if smaller, the element is prefixed as new head of the list; otherwise, insert the element in the rest of the list.
Wrapping up Day 2
As I mentioned yesterday, Prolog really is different. I suspect many readers among those who had no previous exposure to this language must have been left rather confused. The problem is that Prolog introduces a lot of features (unification, pattern matching, backtracking) that are unusual in mainstream languages. Perhaps a different order in the languages (Erlang first?) would have helped assimilate some of these features before tackling backtracking.