These are the notes accompanying the slides A "Hands-on" Introduction to Pure Logic Programming (2_logic_programming.pdf) of the Computational Logic course.
This is the lecture version: it follows the slides in order, but keeps much more of what was actually said in class — the asides, the reasons, the second explanations, and the things that were only ever said out loud. Where the slide gives a bullet, the lecture usually gave a paragraph and an example.
How to use this interactive document:
The code boxes, marked by a question mark (?) in the right top corner, allow interaction. Feel free to edit any program and make all the queries you want!
This is a hands-on introduction to pure logic programming. There are many ways to start; we take the most direct one — give the syntax, explain the operational semantics in the shortest way possible, and get to programming as soon as we can.
Variables start with an uppercase character or an underscore, and may contain underscores and digits: X, Im4u, A_little_garden, _, _x, _22.
If you want a variable whose name would otherwise start with a small letter, put an underscore in front — _x. And _ on its own is a special one: the anonymous variable. You may have met it in other languages; a lot of them have it, and it came originally from Prolog. It is a variable that is different from every other variable. Use it when you need a variable in a position but it will not share with anything else and you do not want to name it.
Constants have a lowercase first character and may contain underscores: a, dog, a_big_cat. Numbers are constants too — 23, and floating point numbers later. If you want a constant that starts with a capital, you cannot write it directly, because that would be a variable; you quote it: 'Hungry man'.
And a great many symbols are constants as well. Pretty much anything that does not start with an underscore or a capital is a constant. For example [], the empty list, is a constant — no different from a, just two characters.
Structures are a functor — the structure name, which looks like a constant — followed by a fixed number of arguments in parentheses, separated by commas:
date(monday, Month, 1994)The functor is date and there are three arguments: monday, Month, 1994. Think of it as a piece of memory that starts with date and then holds three things. The arguments in turn can be variables, constants, or again structures — so these nest as deeply as you like.
Arity is the number of arguments. date(monday, Month, 1994) has arity 3. Functors are usually written name/arity, so this one is date/3; the arity matters in many contexts.
And now a nice consequence: you can think of constants as functors of arity zero, with no arguments. That blends the two ideas rather elegantly — a constant is just a structure that happens to have no arguments. So the constant a is the functor a/0.
Some examples, and one that is not allowed:
| term | what it is |
|---|---|
| dad | a constant, i.e. the functor dad/0 |
| time(min, sec) | a structure, functor time/2 |
| tiger(Hobbes) | a structure, functor tiger/1 |
| Tee(Alf, rob) | ILLEGAL |
| A_good_time | a variable |
Why is Tee(Alf, rob) illegal? Because the functor starts with a capital letter, so it would be a variable — and a variable in functor position takes us out of first-order logic into higher-order logic. (In Ciao you actually can do that, because Ciao supports higher order; but that is a different story, and it comes much later.)
Take +. It is a constant, so it can be a functor, +/2. Declared as an infix operator, you may write
a + b which is exactly the term '+'(a,b)The quotes may or may not be needed depending on context. Likewise, if - is declared prefix then - b is '-'(b); if < is declared infix then a < b is '<'(a,b). And if you declared father infix, you could write john father mary for father(john, mary).
?- X = a+b.Expected answer:
X = a+b ?Now — is a a constant or a variable? A constant. And a+b is simply the structure a+b. Watch:
?- X = +(a,b).Expected answer:
X = a+b ?The same answer. You typed the canonical form and the system printed the operator form, because + is declared infix and printing uses the operator declarations to make the output easier to read.
The same thing with write/1, which is perhaps clearer because no variable is involved:
?- write(+(a,b)), nl.Expected answer:
a+b
?- write(foo(a,b)), nl.Expected answer:
foo(a,b)foo is not declared as an operator, so it is printed the ordinary way. And reading works the same way round — write(a+b) is fine, but:
?- write(a foo b).Expected answer:
{SYNTAX ERROR: (lns 2-1) , or ) expected in arguments
write( a
** here **
foo b ) .
}A syntax error, exactly where foo appears, because foo is not an infix operator. We will assume from here on that the usual operator definitions — +, - and the rest of the standard ones — are always preloaded.
That is the whole of the syntax of data: understanding the data structures of the language and how we write them down. It is not a big deal, but it is worth understanding perfectly.
A rule has this shape:
p₀(t₁, ..., tₙ) :- p₁(...), p₂(...), ..., pₘ(...).The :- is an arrow backwards. Everything in it looks like a term: p₀ is a functor — though with a special name, a predicate symbol — and the ts are its arguments, which may be variables, constants or structures. The predicate symbols themselves cannot be variables; they have to be constants.
The vocabulary:
A fact is a special kind of rule: one with an empty body. When the body is empty we do not write the arrow either — we could write it with nothing after it, but that is a waste of time.
meal(soup, beef, coffee). % a fact meal(First, Second, Third) :- % a rule appetizer(First), main_dish(Second), dessert(Third).Facts are things that are true. Notice that each of these looks like a structure — like a term.
Facts and rules together are called clauses. The piece of program above has two clauses: a fact and a rule.
pet(X) :- animal(X), barks(X). pet(X) :- animal(X), meows(X). pet(X) :- animal(X), roars(X), small(X). animal(tim). animal(spot). animal(hobbes).How many predicates are there here? The tempting answer is three, and it is wrong: there are two. The first three clauses all have predicate name pet and arity one, so they are one predicate. The last three are facts — but facts are clauses too — with predicate name animal and arity one, so they are a second.
Two predicates means two procedures, like f and g in an imperative language. pet/1 is a procedure with three cases; it is a bit like a case statement, but it is a procedure, and you call it by name with arguments, exactly as you would elsewhere.
A program is a set of predicates. The program above is one program, composed of two predicates, each of which has three clauses — pet/1 with one fact and two rules if you fill in the third clause differently, animal/1 with three facts.
This terminology is worth learning properly, because it is used constantly from here on.
:- pet(X).It is a question put to the program. Most systems write it with a question mark, ?-, to make that even clearer — and that is what the Ciao prompt is:
?- pet(X).
For those who have seen resolution theorem proving: to prove something you first negate it and then derive a contradiction. The negation is why the goal sits on that side of the arrow. The question mark is the negated conclusion.
It means two things, and here is something we will meet all the time: there is the logical view and the procedural or program view. This is at the same time a piece of logic and a program that runs. From the point of view of logic, what it says is called the declarative meaning.
Facts state things that are true. Remember that a fact has an empty body, and an empty body is like putting true there — it is the same thing. true is true; you do not have to prove it. So a fact says p if nothing: it is true by itself, with no conditions. When we write animal(spot) we are saying "spot is an animal".
In rules, the commas are conjunction, and :- is classical implication. So
p :- p₁, ..., pₘ. means p ← p₁ ∧ ... ∧ pₘIf p₁ and p₂ and ... and pₘ are true, then p is true. Or, reading it backwards, p is true if all of them are.
So pet(X) :- animal(X), barks(X). reads as "X is a pet if it is an animal and it barks". That is exactly what it means from the logical point of view.
pet(X) :- animal(X), barks(X). pet(X) :- animal(X), meows(X).the X in the first clause is not the same X as in the second. They are two different variables that happen to be written the same way.
And every time a clause is used, you can imagine getting fresh copies of its variables — just as calling a procedure gives you a new activation record with fresh locals. Those who have seen resolution before will recognize this as the renaming you apply to a clause before using it, so that the variables of one application do not get confused with those of another.
X is a pet if it is an animal and it barks; or X is a pet if it is an animal and it meows. Two alternative ways of being a pet.
And the same reading applies to a table of facts. animal(tim). animal(spot). animal(hobbes). says that there are three ways of being an animal — or, equivalently, that there are three individuals who are animals. Alternatives, again.
A query asks whether something follows from the program — and, if it does, for which values of its variables.
:- module(_,_,[]). pet(X) :- animal(X), barks(X). pet(X) :- animal(X), meows(X). animal(tim). animal(spot). animal(hobbes). barks(spot). meows(tim). roars(hobbes).Executing a logic program, given a program and a query, is attempting to find an answer to the query, and giving values for the variables in it.
So if we ask pet(X) — does there exist an X that is a pet? who is a pet? — the system tries to find a substitution for X that makes pet(X) true.
Just by looking, intuitively: an animal that barks — animal(spot) and barks(spot), so spot is certainly an answer. An animal that meows — animal(tim) and meows(tim), so tim is another.
The operational semantics is the other question: how does the system compute those answers? How does the program run? That distinction — the obvious logical semantics, and the mechanism that computes it — is one we will keep returning to.
?- barks(X).Expected answer:
X = spot ? ; nospot barks, and nobody else does — the no means there are no more answers.
?- animal(X).Expected answer:
X = tim ? ; X = spot ? ; X = hobbes ? ; noThree animals and no more. And now the interesting one — asking about a particular individual rather than for all of them:
?- pet(tim).Expected answer:
yes
?- pet(hobbes).Expected answer:
no
?- pet(X).Expected answer:
X = spot ? ; X = tim ? ; nospot is a pet and tim is a pet, and furthermore nobody else is — exactly what we worked out by reading the logic. That is the program running.
Keeping those two apart is the whole discipline. From here on, every topic has both readings.
?- use_module('2examples_bfall').and then ask your questions. It is no more complicated than that. There is a separate set of slides, Developing Programs with a Logic Programming System, which covers this properly, and it is worth going through at about this point.Note the declaration at the top of that file:
:- module(_,_,['sr/bfall']).That prefix matters: it puts the system into pure logic programming mode, which is what this whole part of the course is about. What it does exactly is explained later.
The operational semantics is resolution, which we now describe informally in a form you can follow by hand.
Given a query, take the leftmost literal — a procedure call — and look for a clause whose head matches it. Matching here is not equality: it is unification, which is the subject of the next four slides. If a matching clause is found, replace the call by that clause's body, with the matching substitution applied, and carry on. When nothing is left to prove, the query has succeeded, and the accumulated substitution is the answer.
If a call has several matching clauses, that is a choice point: the alternatives are tried in turn. If a call has no matching clause, we backtrack to the most recent choice point and take the next alternative there.
That is the whole mechanism. What makes it work as programming rather than as proof search is what the next slides are about.
That is one operation covering what most languages need half a dozen mechanisms for. And it is one procedure to solve equations on data structures.
Take two equations: X = f(Y) and Y = a. They say X must equal f(Y) and Y must equal a. Unification solves that system: Y is a, which we knew, and for the whole thing to hold together X has to be f(a).
?- X = f(Y), Y = a.Expected answer:
X = f(a), Y = a ?Another one. X = f(Y) and X = f(b) can only be solved by making Y be b:
?- X = f(Y), X = f(b).Expected answer:
X = f(b), Y = b ?
Like many equation-solving procedures, it works by isolating variables and instantiating them with their values.
Said again: given two terms A and B, how can we make them identical by giving values to the variables in them?
More formally, we want to find a variable substitution — X = a is a variable substitution, and a substitution in general is a set of them — a θ such that applying θ to A and applying θ to B makes the two equal. Or, if no such substitution exists, to fail: to say that this is impossible.
There are two rules in the process:
| A | B | θ | Aθ = Bθ |
|---|---|---|---|
| dog | dog | {} | dog |
| X | a | {X = a} | a |
| X | Y | {X = Y} | Y |
| f(X, g(t)) | f(m(h), g(M)) | {X = m(h), M = t} | f(m(h), g(t)) |
The first is already identical, so nothing has to be done — the substitution is empty, and applying nothing to either side gives dog. The second gives X the value a; B was already a and stays put, and A, which was X, becomes a. The third makes one variable be the other — a bit trickier, since the variables are confusing at first, but you get the hang of it.
The fourth is the first genuinely non-trivial one, and worth walking through slowly. Start on the left: f against f, fine. First argument: X against m(h). These will never be identical unless X is m(h), so that is the first substitution. Continue: g against g, fine. Inside them, t against M — and since we may replace a variable by a constant or a structure, and nothing else, we set M = t.
Now apply the substitution to both sides and check. A becomes f(m(h), g(t)). B becomes f(m(h), g(t)). Identical. It worked.
?- f(X, g(t)) = f(m(h), g(M)).Expected answer:
M = t, X = m(h) ?
?- f(X, g(t)) = f(m(h), t(M)).Expected answer:
no
And a second, quite different way. Try to unify f(X,X) with f(Y,l(Y)). First arguments: X against Y, so X = Y, fine. Second arguments: X against l(Y) — but X is already Y, so what we are really being asked for is
Y = l(Y)which means Y = l(Y) = l(l(Y)) = l(l(l(Y))) = ... — an infinite term.
This case has a name: the occurs check. In classical first-order logic it is not allowed. In modern logic programming systems it often is, and what you get is a cyclic term: a data structure containing a pointer back into itself. Ciao allows it, and so do many current Prologs — but it is a question you have to ask of any system you use, because classical logic says no. Slide 15 comes back to this.
Take the pair from the previous slide, f(X, g(t)) against f(m(H), g(M)). One solution is {X = m(a), H = a, M = t}: apply it and both sides become f(m(a), g(t)). Identical — so it is a correct unifier. But look at what it did: it invented a value for H that nothing in the problem required.
The other solution is {X = m(H), M = t}, which leaves H alone entirely. Both make the terms equal; they are quite different substitutions.
That is what "minimal values" meant on the previous slide, and it matters, because committing early to a value that was not forced would lose solutions.
?- p(X,f(b)) = p(a,Y).Expected answer:
X = a, Y = f(b) ?Straightforward: X meets a, and Y meets f(b).
?- p(X,f(Y)) = p(a,g(b)).Expected answer:
noX = a is fine, but then f(Y) meets g(b): different functors, so nothing can make them identical. Fail.
?- p(X,X) = p(f(Z),f(W)).Expected answer:
X = f(W), Z = W ?Here X occurs twice. Unifying the first arguments makes X be f(Z); unifying the second then forces f(Z) and f(W) to be identical, hence Z = W. The answer is a constraint between Z and W — neither of them has a value.
?- p(X,f(Y)) = p(Z,X).Expected answer:
X = f(Y), Z = f(Y) ?X = Z from the first argument; then f(Y) meets X, which is Z, so both become f(Y).
?- p(X,f(X)) = p(Z,Z).Expected answer:
X = f(X), Z = f(X) ?The last one is the interesting one. X = Z from the first argument; then f(X) meets Z, i.e. f(Z) meets Z. Look at X = f(X): there is no finite term satisfying it — it would have to be f(f(f(...))) forever. What Prolog has built is a cyclic term.
Standard Prolog omits it, because it costs time on every single unification and almost never matters. It is available when you want it:
:- module(_,_,[]). :- use_module(library(iso_misc)). check(X, Z) :- unify_with_occurs_check(p(X,f(X)), p(Z,Z)).
?- check(X, Z).Expected answer:
noWhich is the answer logic wants. Can you explain the difference?
Input and output. The input is a logic program P and a query Q. The output is Qμ, where μ is an answer substitution — values for the variables that appear in the query, which Prolog prints as A = something, B = something. You get that if Q is provable from P, that is, if Q is a logical consequence of P. Otherwise you get failure, which may be a finite failure or an infinite one.
The algorithm. There is one intermediate data structure, called the resolvent, R. Initialize it to the query Q. Then, while R is not empty:
When R becomes empty, we have a proof, and the composition of all the θs, restricted to the query's variables, is the answer.
C1: pet(X) :- animal(X), barks(X). C2: pet(X) :- animal(X), meows(X). C3: animal(spot). C6: barks(spot). C4: animal(tim). C7: meows(tim). C5: animal(hobbes). C8: roars(hobbes).and the query ?- pet(P).
Start. R = pet(P), Q = pet(P).
Step 1. There is only one literal in R, so no choice about which to take. Look for a clause whose head matches pet(P): both C1 and C2 do, so this is a choice point — mark it with a star. Say we take C2. Rename it first, since this is our first use: pet(X1) :- animal(X1), meows(X1). Unifying pet(P) with pet(X1) gives θ = {P = X1}. Apply θ to the query, giving pet(X1), and replace the selected literal by the clause body:
Q = pet(X1) R = animal(X1), meows(X1) C2* {P = X1}Step 2. Leftmost literal is animal(X1). Three clauses match — another choice point. Take C4, animal(tim). It has no variables, so no renaming is needed. θ = {X1 = tim}. It is a fact, so its body is empty and nothing is added; the literal simply disappears:Q = pet(tim) R = meows(tim) C4* {X1 = tim}Step 3. meows(tim) matches only C7, so there is no choice point here — no star. The two are already identical, so θ is empty, and C7 is a fact:
Q = pet(tim) R = (empty) C7 {}R is empty. We have proved the query, and the answer is P = tim.That is resolution, done by hand. And it is exactly what the system does:
:- module(_,_,[]). pet(X) :- animal(X), barks(X). pet(X) :- animal(X), meows(X). animal(tim). animal(spot). animal(hobbes). barks(spot). meows(tim). roars(hobbes).
?- pet(P).Expected answer:
P = spot ? ; P = tim ? ; noThe system's first answer is spot, not tim — it made a different choice at the starred points. Typing ; sends it back to the most recent star to take the next alternative, and the second answer is the one we found by hand.
The choice of clause defines the alternative paths to be explored, and execution explores that tree — depth-first, breadth-first, or otherwise. Since the schematic interpreter leaves the choice open, any logic programming system has to supply one:
Choosing a different clause changes the order in which solutions appear. Both of these are valid executions of the same program and the same query:
?- pet(X). ?- pet(X). X = spot ? ; X = tim ? ; X = tim ? ; X = spot ? ; no noNeither is more correct than the other — the set of solutions is the same, which is what the logic determines. Only the order differs, and the order is a matter of control.
The computation rule has a freedom of its own. We said "take the leftmost literal", but any literal would do — and you could even take several at once, which is where parallelism comes from.
The search rule is the one that matters, because it decides how you explore a tree of possibilities — and, as the next slides show, whether you find the answers at all.
The slides trace two of them side by side for ?- pet(X)., showing at each step the query, the resolvent, the clause chosen and the substitution. The first trace takes clause animal(tim) and reaches barks(tim), which has no matching clause: failure.
The starred entries in those tables are the choice points — the calls where other clauses were still applicable. Failure is not the end of the story: we go back to the last choice point and try the next alternative. Taking animal(spot) instead, barks(spot) does match, and the query succeeds with X = spot.
Typing ; at the ? prompt asks the system to go back and explore yet another branch.
When you call a procedure in any language, you push an activation record — at minimum a return address — onto a stack. That is the forward continuation: when I finish, where do I go? In a clause p :- q, r. the forward continuation of the call to q is "the point just before r".
Prolog has that, and it has a second one. Suppose p has another clause below this one. Then while we are inside q there are two ways out: succeed, and go forward to r; or fail, and go back to p's next clause. So on entering a procedure that has alternatives, Prolog also pushes a backward continuation onto a different stack — the choice-point stack. That, plus a few details, is all backtracking is.
This was David Warren's insight when he built the first Prolog compiler. Until then, implementations represented proof trees and resolution trees explicitly — faithful to the theorem-proving theory, and enormously slow. Warren saw that this is no different from C, except that every now and then it backtracks, and reused the whole machinery of imperative language implementation, adding only what was needed for backtracking. That is why Prolog is a programming language and not a theorem prover.
When you call a procedure in any language you push an activation record — at minimum a return address. That is the forward continuation: when I finish, where do I go? In
p(X,Y) :- q(X,Z), r(Z,Y).the forward continuation of the call to q is "the point just before r".
Prolog has that, and a second one. If p has another clause below this one, then while we are inside q there are two ways out: succeed and go forward to r, or fail and go back to p's next clause. So on entering a procedure that has alternatives, Prolog also pushes a backward continuation onto a different stack — the choice-point stack. That, plus some detail, is all backtracking is.
This was David Warren's insight when he built the first Prolog compiler. Until then, implementations represented proof trees and resolution trees explicitly — faithful to the theorem-proving theory, and enormously slow. Warren saw that this is no different from C, except that every now and then it backtracks, and reused the machinery of imperative language implementation, adding only what backtracking needs. That is why Prolog is a programming language and not a theorem prover.
The slide draws the distinction precisely, and it is worth having the vocabulary.
Programs without search — those that do no "deep" backtracking. In practice that means a program whose procedures have either a single clause, or several of which only one is ever selected for any given call, so the clauses act as a case statement rather than as alternatives.
Programs with search — those with at least one procedure where more than one clause is selected for some calls. These perform search, by backtracking or by another search rule, and they have no direct counterpart in imperative or functional programming.
Take the pets program and the query pet(X). First there is a choice of clause for pet — the barks one or the meows one. Then animal(X) has three possibilities in each branch. Then barks(X) has one, and meows(X) has one. The points where the tree splits are the choice points; where a call has only one matching clause there is no real choice.
A different query gives a different tree. Ask pet(spot) instead. At the pet call there are still two choices, but at animal(spot) there is now only one, because X is already instantiated: animal(tim) and animal(hobbes) will not match. So the tree depends on the question — and for the same question, on the bindings of its variables.
The search and computation rules explain how this tree is explored. A search rule that takes the first clause, then the second, explores the leftmost branch first, then the next — which is depth-first.
Go back to semi-decidability, the general limitation we met in the introduction: if you ask a question that has an answer, you will find it in finite time; if the answer is no, you may find that out in finite time, or you may not.
In terms of the search tree that means:
The picture shows the three cases: a tree that is finite and has solutions, a tree that is finite and has none, and an infinite tree, where a solution may sit to the right of a branch that never ends.
That is the fact everything else in this section follows from.
Breadth-first search explores the tree level by level: one step down this branch, one step down that one, then the next level everywhere, and so on. Since every solution is at finite depth, it will reach every one of them before falling down an infinite branch — breadth-first is complete.
The price is in time and, above all, memory: to be at any point in the exploration you must have the whole tree above you stored somehow, however compressed. Depth-first needs one branch; this needs the whole frontier.
Depth-first is what Prolog does by default, and for a good reason: it is efficient, and Prolog is meant to be a programming language. There are ways of reordering clauses so that infinite branches move to the right and depth-first finds the solutions first — but that requires understanding what is going on and thinking about it. That is the second part of the course. Here we can ignore it.
Ciao lets you choose, per module:
:- module(_, _, [sr/bfall]). % breadth-first: complete, slower, more memory :- module(_, _, []). % default: Prolog's depth-first
In practice you often want only some predicates to run breadth-first, and there are finer-grained ways to say so; bfall is the maximally simple choice — everything, no exceptions.
Ciao has other search rules too, iterative deepening among them: go to a fixed depth on one branch, then to the same depth on the next, increasing the bound each round. It is essentially breadth-first but far cheaper in memory. For simplicity everything here uses sr/bfall.
Try it on the pets program: switch its declaration between [sr/bfall] and [], reload, and ask pet(X) again. You get exactly the same answers — because that program is finite and has no infinite branches. To get an infinite branch you need recursion and data structures that can grow without bound. It is the complicated cases where the choice matters.
Two orderings are under your control, and both change how much work gets done.
The order of goals in a body. Consider:
:- module(_,_,[]). :- use_module(library(system),[pause/1]). a(X,Y) :- p(X), q(X,Y). % p first, then q b(X,Y) :- q(X,Y), p(X). % q first, then p p(4). p(5). q(1, a) :- lots_of_computing. q(2, b) :- lots_of_computing. q(4, c) :- lots_of_computing. q(4, d) :- lots_of_computing. lots_of_computing :- pause(3).a/2 and b/2 are logically identical — the conjunction is the same. But a/2 calls p(X) first, binding X to 4, so the call to q is q(4,Y) and only the matching clauses are tried. b/2 calls q first with X unbound, so it works through q(1,a) and q(2,b) — each costing three seconds of lots_of_computing — before reaching one that p accepts.
Same logic, same answers, very different amount of work. This is the generate-and-test versus constrain-and-generate distinction that the CLP deck later builds a whole chapter on.
Now ?- q(X,Y), p(X). We call q with X free. First clause: X = 1, Y = a, do the work — then return, call p(1), and fail. The whole computation was wasted. Second clause: X = 2, do the work, fail again. Third clause: X = 4, do the work, and p(4) finally succeeds. Three expensive computations instead of one.
If you want all the solutions, that is only a change of order — you must explore the whole tree either way. But if you want one, it can matter enormously. Imagine the solution sits on a short branch: at the left you find it at once and stop; at the right you grind through the whole useless part of the tree first.
And there is a third mechanism, which pure logic programming does not have: the pruning operators, the cut, which removes parts of the tree outright. That is the Prolog deck's subject.
:- module(_,_,[]). animal(dog(barry)). named(dog(Name), Name).
?- animal(A), named(A, Name).Expected answer:
A = dog(barry), Name = barry ?Now say what happened in programming terms. Calling animal(A) creates a data structure, dog(barry) — an object with barry inside it — and makes A point at it. Then named(A,Name) is called with A pointing at dog(barry), and the pattern in the head, dog(Name), matches against it: it reaches inside the structure and binds Name to barry. That Name is the same variable as the second argument, so the value is passed back out, and comes out as the answer.
You can build lists, trees, and any data structure you like on this basis. The whole data structures course can be done with unification and first-order terms. That is the beauty of the approach.
In a functional language the arguments are all inputs and the return value is the output — a bunch of inputs and one output. Here it is not like that. Is the argument of pet/1 an input or an output? It depends. Ask pet(spot) and it is an input: you are handing spot to the procedure and asking it to check. Ask pet(X) and it is an output: you are asking the procedure to produce a value. Same argument, same procedure.
So arguments are bidirectional, and which role they play depends on their instantiation state at the moment of the call.
:- module(_,_,[]). add(0, Y, Y). add(s(X), Y, s(Z)) :- add(X, Y, Z).
?- add(s(0), s(s(0)), Z).Expected answer:
Z = s(s(s(0))) ?
?- add(s(0), Y, s(s(s(0)))).Expected answer:
Y = s(s(0)) ?The same clauses ran forwards and backwards: first as addition, then as subtraction. Nothing in the program declares which argument is the input — because in a relation there is no such thing.
That combination of inputs and outputs has a name: the mode of a call. A three-argument predicate might be called in mode input-input-output, or input-output-input, or output-input-input, and so on. We write modes with + and -:
add(+X, +Y, -Z) X and Y are given; Z is where the answer comes backWe will use this notation from time to time. Note that it describes a call, not the predicate — the same predicate can be used in several modes.
Everything so far has been pure: no cut, no assert, no arithmetic evaluation, no negation. What we have is Horn clauses plus unification plus a search rule — and that is enough to program with, as the rest of this deck shows.
:- module(_,_,[sr/bfall]).
father_of(john, peter).
father_of(john, mary).
father_of(peter, michael).
mother_of(mary, david).
mother_of(jill, john).
grandfather_of(L,M) :-
father_of(L,N),
father_of(N,M).
grandfather_of(X,Y) :-
father_of(X,Z),
mother_of(Z,Y).The facts are the table; grandfather_of/2 is a view derived from it — and note that it takes two clauses, because a grandfather may be reached through a father or through a mother. Your grandfather can be your grandfather on your father's side or on your mother's side, and the two clauses say exactly that.Or, in plainer words: L is the grandfather of M if L is the father of some N, and that N is the father of M.
Queries first. Facts already in the program are easy:
?- father_of(john, peter).Expected answer:
yesAnd what is not in the program is answered no:
?- father_of(john, david).Expected answer:
no
?- father_of(john, X).Expected answer:
X = peter ? ; X = mary ? ; no
?- grandfather_of(X, Y).Expected answer:
X = john, Y = michael ? ; X = john, Y = david ? ; noTwo grandfather relationships in the database, found without either argument being given.
Both directions work equally well. Ask who is the grandfather of michael and you get john — via john, peter, michael — and no one else:
?- grandfather_of(X, michael).Expected answer:
X = john ? ; noAsk instead who john is the grandfather of, and there are two answers, one down each rule: john, peter, michael and john, mary, david.
?- grandfather_of(john, X).Expected answer:
X = michael ? ; X = david ? ; no
A second example makes the point that "database" need not mean people and dates. Here the facts are the topology of a circuit — which resistors and transistors connect which nodes — and the rules are a little bit of circuit theory, saying what an inverter, a NAND gate and an AND gate look like in terms of that topology:
:- module(_,_,[sr/bfall]). resistor(power,n1). resistor(power,n2). transistor(n2,ground,n1). transistor(n3,n4,n2). transistor(n5,ground,n4). inverter(Input,Output) :- transistor(Input,ground,Output), resistor(power,Output). nand_gate(Input1,Input2,Output) :- transistor(Input1,X,Output), transistor(Input2,ground,X), resistor(power,Output). and_gate(Input1,Input2,Output) :- nand_gate(Input1,Input2,X), inverter(X, Output).
?- and_gate(In1, In2, Out).Expected answer:
In1 = n3, In2 = n5, Out = n1 ?The program has found a gate inside the circuit: it recognized that this particular arrangement of five components is an AND gate, and told us which nodes are its inputs and its output. Nobody wrote a search for gates — the rules say what a gate is, and the search comes free.
course(complog,wed,18,30,20,30,'M.','Hermenegildo',new,5102).and then ask when it is with
?- course(complog,Day,StartH,StartM,FinishH,FinishM,C,D,E,F).which works, and is horrible. There is no structure at all.
Alternatively, structure it:
:- module(_,_,[]).
course(complog, Time, Lecturer, Location) :-
Time = t(wed, 18:30, 20:30),
Lecturer = lect('M.','Hermenegildo'),
Location = loc(new, 5102).The time is now a data structure with three fields — day, start, finish — and note the : being used as an infix operator: 18:30 is really the term :(18,30), but written the way you would want to write it.course(complog, t(wed,18:30,20:30), lect('M.','Hermenegildo'), loc(new,5102)).X = Y behaves as if defined by the single fact '='(X,X). — one clause, whose two arguments are the same variable. That is the whole definition of equality here. ?- course(complog, Time, A, B).Expected answer:
A = lect('M.','Hermenegildo'),
B = loc(new,5102),
Time = t(wed,18:30,20:30) ?And a small trick: naming a variable with a leading underscore tells the top level not to print it. Better still, use the anonymous variable and do not name it at all:?- course(complog, Time, _, _).Expected answer:
Time = t(wed,18:30,20:30) ?Notice what was involved in building loc(new,5102): no type declaration, no new, no allocation. The structure is created on the fly, by unification.
Now the general point. Terms are the only data structure, and they are enough. A structure groups related things: date(monday, Month, 1994) is a record with three fields; person(name(john,smith), age(23)) nests records inside records.
Two habits are worth forming early.
Use the anonymous variable for fields you do not care about. If you want the year out of a date, write date(_, _, Year) rather than inventing names you never use. Each _ is a different fresh variable, which is exactly what you want here.
Access fields by unification, not by taking things apart. To get at the year you do not call an accessor; you unify the whole term with a pattern and let the year fall out. That is what "data abstraction" means in this setting: the pattern is the accessor.
The = predicate deserves a note. X = Y is not assignment and not a test — it is a call to the unification algorithm, exactly as on slide 11. It succeeds if the two can be made identical, and binds whatever it must to achieve that.
That is why unification is cheap, and why "modifying" a data structure is in quotes: you never overwrite anything, you bind variables that were left unbound.
The slide draws the heap after
main :- X = f(K,g(K)), Y = a, Z = g(L), W = h(b,L), % heap memory at this point ---> p(X,Y), q(Y,Z), r(W).
Note the aliasing: K occurs twice in X, so both places point at the same cell, and L is shared between Z and W. Calling p(X,Y) passes pointers to these structures, not copies. Which is the summary: terms are data structures with pointers, and logical variables are declarative pointers — they can only be assigned once.
| relational model | logic programming |
|---|---|
| relation / table | predicate defined by facts |
| tuple / row | a fact |
| attribute / column | an argument position |
| view | predicate defined by rules |
| selection | a constant in the head |
| projection | fewer arguments, or _ |
| join | a shared variable |
The last one is the pretty one. In grandfather_of(L,M) :- father_of(L,N), father_of(N,M). the shared N is the join: the two tables are joined on the column where the variable is repeated. You do not write a join operator; you write the same variable twice.
The field has grown languages of its own, and they are deliberately less powerful than Prolog. They take a subset of definite programs and restrict the syntax:
What the restrictions buy is reasoning. These systems support much stronger notions of negation, under the stable model semantics, which is the basis of Answer Set Programming (ASP): you state what holds and what cannot hold, and the system returns the models that satisfy it all. For knowledge representation and reasoning that is a very effective way to write a problem down.
:- module(_,_,[]). parent(X,Y) :- father_of(X,Y). parent(X,Y) :- mother_of(X,Y). ancestor(X,Y) :- parent(X,Y). ancestor(X,Y) :- parent(X,Z), ancestor(Z,Y). father_of(john, peter). father_of(john, mary). father_of(peter, michael). mother_of(mary, david). mother_of(jill, john).Read it as the definition it is: X is an ancestor of Y if X is a parent of Y, or if X is a parent of someone who is an ancestor of Y. Two clauses: the base case and the recursive case. That shape — one clause that stops, one that reduces the problem — recurs in every recursive predicate in this deck.
?- ancestor(john, X).Expected answer:
X = peter ? ; X = mary ? ; X = michael ? ; X = david ? ; noAll four descendants of john, at any depth — which is exactly what relational algebra cannot do.
The pure answer is to define the complement as a relation, positively. For naturals in Peano form:
:- module(_,_,[]).
person_num(john, 0).
person_num(peter, s(0)).
person_num(mary, s(s(0))).
person_num(michael, s(s(s(0)))).
person_num(david, s(s(s(s(0))))).
person_num(jill, s(s(s(s(s(0)))))).
diffnat(0, s(_)). % 0 differs from any s(...)
diffnat(s(_), 0). % any s(...) differs from 0
diffnat(s(X), s(Y)) :- diffnat(X,Y). % same depth: look further in
different_person(X,Y) :-
person_num(X,NX),
person_num(Y,NY),
diffnat(NX,NY).Read diffnat/2 as a definition of difference rather than as a test: two Peano numbers differ if one is zero and the other is not, or if both are successors whose predecessors differ. Three clauses, no negation anywhere.?- different_person(john, mary).Expected answer:
yes
?- different_person(john, john).Expected answer:
no
And note the other half: to say two things are equal, you need no test at all. Use the same variable twice and unification does it.
Keep those two apart: the type is the set, the definition is the program.
Take weekday. The set of terms is 'Monday', 'Tuesday', … — in quotes, because they start with a capital and are constants, not variables. The definition is seven facts.
Now a structured type. A date is a weekday together with a day of the month, so the set of terms we want is date('Monday',23), date('Tuesday',24) and so on — and the definition is one clause using two other types:
:- module(_,_,[sr/bfall]).
weekday('Monday'). weekday('Tuesday'). weekday('Wednesday').
weekday('Thursday'). weekday('Friday'). weekday('Saturday').
weekday('Sunday').
day_of_month(1). day_of_month(2). day_of_month(31).
date(date(W,D)) :- weekday(W), day_of_month(D).?- date(date('Monday',31)).Expected answer: yes
?- date(date('Monday',23)).Expected answer: nono — because only 1, 2 and 31 were declared as days of the month in this abbreviated version. And date(D) with D free generates dates: Monday the 1st, Monday the 2nd, Monday the 31st, Tuesday the 1st, and so on.
They are not functions. They are not called, not interpreted, they do not run. nat/1 on the other hand is a predicate: it is called, it runs, it does things.
:- module(_,_,[sr/bfall]). nat_num(0). nat_num(s(X)) :- nat_num(X). % Integers: a natural, possibly with a minus in front. pint( X) :- nat_num(X). pint(-X) :- nat_num(X).Two clauses: one unit clause and one recursive clause with a single body literal. This is about the minimal recursive predicate there is.
?- nat_num(s(s(s(0)))).Expected answer:
yesAnd the same definition generates the naturals if you call nat_num(X) with X free.
Call nat_num(s(s(s(0)))). It does not match nat_num(0). It matches nat_num(s(X)) with X = s(s(0)), and calls nat_num(s(s(0))). That matches again with X = s(0), then again with X = 0, and finally nat_num(0) matches the base case. Done.
How many steps? As many as there are ss — one per level, plus the base case. So in this mode — argument fully instantiated — the predicate is linear, and it is a loop: walk down the structure checking that everything is an s and that a 0 sits at the bottom. You could not do it in less: to be sure the term really is a natural number you have to look at all of it.
Then type ;. The system backtracks to the choice it left behind, takes the second clause instead (with the variable renamed to X1), binds X to s(X1), calls nat_num(X1), which matches the base case — and answers s(0). One step.
So generating solutions costs one step per solution, not n steps for the nth. Different mode, different cost; and in both cases the cost is something you can work out, exactly as in an ordinary program.
One base case, one step case. The same two-clause shape as ancestor/2, and as everything that follows.
:- module(_,_,[sr/bfall]). nat(0). nat(s(X)) :- nat(X). less_or_equal(0,X) :- nat(X). less_or_equal(s(X),s(Y)) :- less_or_equal(X,Y). plus(0,Y,Y) :- nat(Y). plus(s(X),Y,s(Z)) :- plus(X,Y,Z).
And note that it is a predicate, a relation — not a function — which is why it can be called in so many ways:
?- less_or_equal(s(0), s(s(0))).Expected answer:
yes
?- less_or_equal(X, 0).Expected answer:
X = 0 ? ; noZero, and nothing else — correct. Used the other way it becomes a generator: ask for everything less than or equal to two and you get exactly three answers.
?- less_or_equal(X, s(s(0))).Expected answer:
X = 0 ? ; X = s(0) ? ; X = s(s(0)) ? ; noYou can even leave both arguments free and ask for the relation itself, less_or_equal(X,Y). That set is infinite, so it never finishes — but because we are running breadth-first the enumeration is a good one, hitting all the cases in order: (0,0), (0,1), (1,1), (0,2), (1,2), (2,2), … Under depth-first it would go down one branch forever.
?- plus(s(0), s(s(0)), Z).Expected answer:
Z = s(s(s(0))) ?And with the result given instead, it enumerates every way of reaching it:
?- plus(X, Y, s(s(0))).Expected answer:
X = 0, Y = s(s(0)) ? ; X = s(0), Y = s(0) ? ; X = s(s(0)), Y = 0 ? ; noThree ways to make 2, and then no — it knows it has found them all. One definition of addition is simultaneously addition, subtraction, and the enumeration of partitions.
Subtraction proper:
?- plus(X, s(0), s(s(s(0)))).Expected answer:
X = s(s(0)) ?And a question with no answer gets no, correctly — there is no number that added to 2 gives 1:
?- plus(X, s(s(0)), s(0)).Expected answer:
no
plus(X,0,X) :- nat(X). plus(X,s(Y),s(Z)) :- plus(X,Y,Z).— recursing on the second argument instead of the first. It computes the same relation, and some find it easier to read.
What you should not do is put both definitions in the same program. The meaning would be the same — the same set is computed — but you would get several proof trees for the same query and each solution computed more than once. Superfluous definitions are inefficient.
The slides suggest writing times/3 (multiplication is repeated addition), exp/3 (exponentiation is repeated multiplication), factorial/2, minimum/3 and mod/3 for yourself. mod/3 is worth stating precisely before programming it: Z is X mod Y when X = Y*Q + Z for some Q, with Z less than Y. Write the mathematics down first and the program tends to follow.
Multiplication follows from addition, and the naturals themselves from nat/1 — building up arithmetic from nothing but Horn clauses. less_or_equal/2 shows the same pattern for the ordering.
Writing plus(X, Y, Z) everywhere is faithful to the relational reading but verbose when you only want a value. Ciao's functional syntax package lets you write the same predicates as functions, without changing what they are:
:- use_package(functional). :- fun_eval plus/2.after which plus(X,Y) in an argument position means "the Z such that plus(X,Y,Z)". The predicate is unchanged; only the notation is.
Ackermann's function is the standard example of something that is easy to define and expensive to compute. Written in a functional language over Peano numbers, it is three equations:
datatype nat = Zero | S of nat fun ack 0 n = S n | ack (S m1) 0 = ack m1 (S 0) | ack (S m1) (S n1) = ack m1 (ack (S m1) n1)A function of two arguments becomes a predicate of three, the extra one holding the result:
:- module(_,_,[]).
ack(0, N, s(N)).
ack(s(M), 0, Val) :-
ack(M, s(0), Val).
ack(s(M), s(N), Val) :-
ack(s(M), N, Val1),
ack(M, Val1, Val).?- ack(s(s(0)), s(s(s(0))), X).
X = s(s(s(s(s(s(s(s(s(0)))))))))Nine s's: A(2,3) = 9, which is the answer the equations give.
Being a predicate rather than a function, it also runs in the other directions, which the functional definition cannot do at all:
?- ack(s(s(0)), Y, s(s(s(s(s(s(s(s(s(0)))))))))).
Y = s(s(s(0)))Which second argument gives 9? — and likewise ?- ack(X, s(s(s(0))), s(s(s(s(s(s(s(s(s(0)))))))))). answers X = s(s(0)).
Slide 56 writes the same three clauses in functional syntax, with s declared as a prefix operator so that the Peano numbers need no parentheses:
:- module(_,_,[]). :- use_package(functional). :- op(100,fy,s). ack( 0, N) := s N. ack(s M, 0) := ack(M, s 0). ack(s M, s N) := ack(M, ack(s M, N) ).This is the same predicate ack/3 as above — := only says that the last argument is the result — so ?- ack(s(s(0)), s(s(s(0))), X). still answers X = s(s(s(s(s(s(s(s(s(0))))))))).
Slide 57 applies the same notation to a type, in four steps. Starting from nat/1, written with := naming the result argument:
nat := 0. nat := s(X) :- nat(X).the ~ operator ("evaluate this and put the result here") removes the auxiliary variable, and inside a predicate's own definition even the ~ can be left out:
nat := 0. nat := s(~nat).
nat := 0. nat := s(nat).
with the prefix operator the parentheses go too, and | writes the two clauses as one:
nat := 0. nat := s nat.
nat := 0 | s nat.
That last line is a type definition as compact as a functional language's datatype, and it means exactly what we started with:
:- module(_,_,[]). :- use_package(functional). :- op(100,fy,s). nat := 0 | s nat.
?- nat(s(s(0))).
yes
A list is a binary structure — a pair whose first argument is an element and whose second is the rest of the list. To build it we need two things:
?- X = '.'(a,'.'(b,[])).Expected answer:
X = [a,b] ?You built a nested pair structure, and the system printed it as [a,b] — because it recognized the dot and applied the sugar.
| formal object | "cons pair" syntax | "element" syntax |
|---|---|---|
| .(a,[]) | [a|[]] | [a] |
| .(a,.(b,[])) | [a|[b|[]]] | [a,b] |
| .(a,.(b,.(c,[]))) | [a|[b|[c|[]]]] | [a,b,c] |
| .(a,X) | [a|X] | [a|X] |
| .(a,.(b,X)) | [a|[b|X]] | [a,b|X] |
The last two rows are the important ones. [a|X] is a list whose first element is a and whose tail is unknown — it cannot be written in pure element syntax, and it does not have to be. [a,b|X] starts with a, then b, and then is open. Lists with a variable tail are extremely useful, and they come back in force in the Prolog deck under the name difference lists.
?- [a,b,c] = [X|Y].Expected answer:
X = a, Y = [b,c] ?Write it in dots if it is not obvious: .(a,.(b,.(c,[]))) against .(X,Y). Dot matches dot, a matches X, and Y matches everything on the right of the first pair — which is the list [b,c].
?- [a] = [a,b|X].Expected answer:
no[a] has one pair; [a,b|X] needs at least two.
And one that catches people. What does X get here?
?- [a,b] = [X|_].Expected answer:
X = a ?a, not [a,b] — [X|_] is a pair, so X is the head. Then a longer one, mixing both notations:
?- [a,b,c,d,e,f,g] = [X,Y|Z].Expected answer:
X = a, Y = b, Z = [c,d,e,f,g] ?
:- module(_,_,[]). list([]). list([_|Y]) :- list(Y). list_member(X, [X|L]) :- list(L). list_member(X, [_|T]) :- list_member(X, T). list_length([], 0). list_length([_|T], s(N)) :- list_length(T, N).One base case, one step case — the same shape as everything so far. And once you see that, every list predicate writes itself: say what holds for [], then say what holds for [H|T] given that it holds for T.
list_member/2 says: X is a member of a list whose head is X, or of a list whose tail it is a member of. list_length/2 counts in Peano numbers, because we are still being pure.
:- module(_,_,[]). list([]). list([_|Y]) :- list(Y). list_append([], L, L) :- list(L). list_append([X|Xs], Ys, [X|Zs]) :- list_append(Xs, Ys, Zs).Two clauses. Appending nothing to L gives L; appending [X|Xs] to Ys gives a list whose head is X and whose tail is Xs appended to Ys.
?- list_append([1,2], [3,4], Z).Expected answer:
Z = [1,2,3,4] ?That is the use you expected. But nothing in the program says which arguments are inputs:
?- list_append(X, Y, [1,2]).Expected answer:
X = [], Y = [1,2] ? ; X = [1], Y = [2] ? ; X = [1,2], Y = [] ? ; noEvery way of splitting [1,2] into two lists — and then no. The same two clauses are concatenation, splitting, prefix-testing and suffix-testing, depending on which arguments you supply.
In between those two extremes there are the intermediate modes, and they are the ones worth trying by hand. Checking a result:
?- list_append([a,b], [1,2], [a,b,1,2]).Expected answer:
yesList subtraction, from either side. What must be appended to [a,b] to give [a,b,1,2]?
?- list_append([a,b], Y, [a,b,1,2]).Expected answer:
Y = [1,2] ?And the mirror image — what must [1,2] be appended to?
?- list_append(X, [1,2], [a,b,1,2]).Expected answer:
X = [a,b] ?
When writing a recursive predicate over a list, ask two questions and write one clause for each:
:- module(_,_,[]).
list([]).
list([_|Y]) :- list(Y).
list_append([], L, L) :- list(L).
list_append([X|Xs], Ys, [X|Zs]) :- list_append(Xs, Ys, Zs).
% naive: reverse the tail, then append the head at the end
reverse_naive([], []).
reverse_naive([X|Xs], Ys) :-
reverse_naive(Xs, Zs),
list_append(Zs, [X], Ys).
% accumulator: carry the answer being built
reverse_acc(Xs, Ys) :- reverse_(Xs, [], Ys).
reverse_([], Ys, Ys).
reverse_([X|Xs], Acc, Ys) :-
NewAcc = [X|Acc],
reverse_(Xs, NewAcc, Ys).The naive one is the direct reading of the definition: to reverse [X|Xs], reverse Xs and then put X at the end. Work one step by hand, assuming the recursion works — which is the way to think about recursive programs. Given [1,2,3], head unification gives X = 1 and Xs = [2,3]. Assume reverse_naive([2,3],Zs) works and gives Zs = [3,2]. Then append the singleton [1] to that, and Ys = [3,2,1]. It works. The base case is easy: reversing [] gives [].Both give the same answer:
?- reverse_naive([1,2,3], Y).Expected answer:
Y = [3,2,1] ?
?- reverse_acc([1,2,3], Y).Expected answer:
Y = [3,2,1] ?And the naive one runs backwards too, which is not obvious:
?- reverse_naive(X, [3,2,1]).Expected answer:
X = [1,2,3] ?
X = [], Y = [] ? ; X = [_A], Y = [_A] ? ; X = [_B,_A], Y = [_A,_B] ? ; ...Those are variables, so each answer stands for infinitely many cases. The third one says that any two-element list reverses by swapping its elements — including reverse_naive([f(a),g(b)], Y), which gives Y = [g(b),f(a)]. The program is enumerating the shape of the relation, not just instances of it.
There is a second, related problem, and it is about the shape of the recursion rather than the count. In reverse_naive/2 the recursive call is not last: after it returns you still have to do the list_append/3. That means the system must remember to come back, which means pushing a frame on the stack for every level of the recursion.
reverse_acc(Xs,Ys) :- reverse_(Xs,[],Ys).
reverse_([],Ys,Ys).
reverse_([X|Xs],Acc,Ys) :-
NewAcc = [X|Acc],
reverse_(Xs,NewAcc,Ys).The NewAcc = [X|Acc] is written out here for clarity; putting [X|Acc] directly in the call is exactly equivalent.Follow [1,2,3] down. The accumulator starts as [].
| Xs | Acc | NewAcc | |
|---|---|---|---|
| call 1 | [2,3] | [] | [1] |
| call 2 | [3] | [1] | [2,1] |
| call 3 | [] | [2,1] | [3,2,1] |
| call 4 | [] | [3,2,1] | — matches reverse_([],Ys,Ys) |
Each step conses the current head onto the front of what has been accumulated. You might object that consing onto the front is not reversing — but notice that the accumulator is being passed downwards, not returned upwards. The element added last ends up at the front, which is exactly the reversal.
And the base case is where it comes back out: reverse_([],Ys,Ys) unifies the finished accumulator with the output argument, and that answer is then handed straight back up through every pending call, unchanged.
:- use_module(engine(runtime_control),[statistics/2]).
:- use_module(library(lists),[length/2]).
rev_naive(N,T) :-
length(L,N),
statistics(walltime, [_,_]),
reverse_naive(L,_RL),
statistics(walltime, [_,T]).statistics(walltime,[_,T]) gives, in its second element, the time since the previous call to statistics/2 — so calling it once to reset, then again after the work, measures the interval.Naive reverse, doubling the input each time (Ciao 1.25, times in milliseconds):
| goal | time (ms) |
|---|---|
| rev_naive(100,T) | 0.043 |
| rev_naive(200,T) | 0.142 |
| rev_naive(400,T) | 0.846 |
| rev_naive(800,T) | 3.274 |
Double the length, roughly quadruple the time. That is quadratic, plainly.
The accumulator version, over a far wider range:
| goal | time (ms) |
|---|---|
| rev_acc(100,T) | 0.002 |
| rev_acc(1000,T) | 0.007 |
| rev_acc(10000,T) | 0.069 |
| rev_acc(100000,T) | 2.608 |
Ten times the length, roughly ten times the time — linear. And note the scale: at 800 elements the naive version already costs more than the accumulator version does at 10,000. This is not a small constant factor; it is a different algorithm.
Some exercises on lists: prefix, suffix, sublist, ... — opens in the Ciao playground, the same link as the button on the slide.
For lists we used a pair — a functor of arity 2. For a binary tree we need arity 3: the contents of the node, the left subtree and the right subtree. And we need a constant for the empty tree, the way [] was the empty list; we call it void, though the name is arbitrary.
A binary tree is empty, or a node with an element and two subtrees:
:- module(_,_,[]).
binary_tree(void).
binary_tree(tree(_Element, Left, Right)) :-
binary_tree(Left),
binary_tree(Right).
tree_member(X, tree(X, Left, Right)) :- binary_tree(Left), binary_tree(Right).
tree_member(X, tree(_, Left, _Right)) :- tree_member(X, Left).
tree_member(X, tree(_, _Left, Right)) :- tree_member(X, Right).Note what the type definition does not say: nothing at all about the element. A node may contain anything. What it constrains is the shape — that the two branches must themselves be binary trees.?- binary_tree(tree(foo, void, tree(bar, void, void))).Expected answer:
yesAnd what is not a tree:
?- binary_tree(tree(a, b, c)).Expected answer:
nob and c are not trees — the same mistake as writing [a|b] and expecting a list. And:
?- binary_tree(tree(a, [], [])).Expected answer:
nono, because the empty tree here is void, not []. The type is exactly what you defined it to be.
T = void ? ; T = tree(_,void,void) ? ; T = tree(_,void,tree(_,void,void)) ? ; T = tree(_,tree(_,void,void),void) ? ; ...The _ says the node contents do not matter — anything of that shape is a binary tree. Under sr/bfall this is a genuinely orderly enumeration of the binary trees, by size.
tree_member/2 needs three clauses where the list version needed two, because there are two subtrees to look in rather than one tail. Otherwise it is the same thought: X is in the tree if it is the contents of this node, or if it is somewhere in the left branch, or somewhere in the right.
?- tree_example(_T), tree_member(e, _T).Expected answer:
noAsking for all the members of the example tree gives a, b, c and b again — the tree contains b twice, and the program finds both occurrences. Logically the two answers are the same answer; operationally it is quite useful to be told there are two of them.
Traversal turns a tree into a list, and the shape of the clause decides which traversal you get:
:- module(_,_,[]).
list([]).
list([_|Y]) :- list(Y).
list_append([], L, L) :- list(L).
list_append([X|Xs], Ys, [X|Zs]) :- list_append(Xs, Ys, Zs).
pre_order(void, []).
pre_order(tree(X,Left,Right), Elements) :-
pre_order(Left, ElementsLeft),
pre_order(Right, ElementsRight),
list_append([X|ElementsLeft], ElementsRight, Elements).
tree_example(tree(a, tree(b, void, void), tree(c, tree(b, void, void), void))).?- tree_example(T), pre_order(T, L).Expected answer:
L = [a,b,c,b], T = tree(a,tree(b,void,void),tree(c,tree(b,void,void),void)) ?Move the X from the front of the append to between the two lists and you have in-order; move it to the end and you have post-order. The traversal is the position of one symbol.
lt_member(X, [X|Y]) :- list(Y). lt_member(X, [_|T]) :- lt_member(X, T). lt_member(X, tree(X, L, R)) :- binary_tree(L), binary_tree(R). lt_member(X, tree(_, L, _)) :- lt_member(X, L). lt_member(X, tree(_, _, R)) :- lt_member(X, R).Nothing had to be declared. The clause heads dispatch on the shape of the term, which is what unification does anyway — so polymorphism here costs exactly nothing. It works on a list, and it works on a tree, and you did not write a word of dispatch code.
T = [M] ? ; T = [M,_] ? ; T = [_,M] ? ; T = tree(M,void,void) ? ; T = tree(_,[M],_) ? ; T = tree(_,_,[M]) ? ; T = [M,_,_] ? ; ...Look at the fifth answer: a tree with a list inside it. And that is correct, given what we wrote. By mixing the clauses of the two definitions into one predicate, we allowed the recursion to cross from one type into the other. What we actually defined is a new type — call it the "list-tree" — in which lists and trees may nest inside each other arbitrarily.
Can you keep the types apart? Certainly — but you have to write it. Keep the two original definitions, each recursing into itself, and put a two-clause wrapper on top:
lt_member_separate(X,Y) :- list_member(X,Y). lt_member_separate(X,Y) :- tree_member(X,Y).Now once you are inside tree_member/2 you can only be in a tree, and once you are inside list_member/2 you can only be in a list. Generating with both arguments free gives lists and trees, and never a mixture. Two designs, two different types; you choose by choosing what to write.
:- module(_,_,[sr/bfall]). polynomial(X,X). polynomial(Term,_X) :- pconstant(Term). polynomial(Term1+Term2,X) :- polynomial(Term1,X), polynomial(Term2,X). polynomial(Term1-Term2,X) :- polynomial(Term1,X), polynomial(Term2,X). polynomial(Term1*Term2,X) :- polynomial(Term1,X), polynomial(Term2,X). polynomial(Term1/Term2,X) :- polynomial(Term1,X), pconstant(Term2). polynomial(Term1^N,X) :- polynomial(Term1,X), nat_num(N). pconstant(X) :- nat_num(X). pconstant(a). pconstant(b). pconstant(c). nat_num(0). nat_num(s(X)) :- nat_num(X).
?- polynomial(a * x ^ s(s(0)) + b, x).Expected answer:
yesa is a constant here because we declared it one, so a*x² + b is a polynomial in x. And the definition is precise enough to reject things:
?- polynomial(a * x ^ x + b, x).Expected answer:
nox^x is not a polynomial — the exponent has to be a natural number, and the clause says so.
As always, the same definition enumerates: polynomial(P,x) produces x, a, b, c, 0, x+x, x-x, and onwards through the polynomials in x.
poly_f(X) := X | ~pconst_f | ~poly_f(X) + ~poly_f(X) | ~poly_f(X) - ~poly_f(X) | ~poly_f(X) * ~poly_f(X) | ~poly_f(X) / ~pconst_f | ~poly_f(X) ^ ~nat_num. pconst_f := ~nat_num | a | b | c.That is about the shortest way to write down what a polynomial is — and it remains, simultaneously, a checker, a generator, and a piece of a working program. (Note that the arguments come out reversed: the functional definition is called as poly_f(x, Expression).)
:- module(_,_,[sr/bfall]). deriv(X, X, s(0) ). deriv(C, _X, 0 ) :- pconstant(C). deriv(U+V, X, DU+DV ) :- deriv(U,X,DU), deriv(V,X,DV). deriv(U-V, X, DU-DV ) :- deriv(U,X,DU), deriv(V,X,DV). deriv(U*V, X, DU*V+U*DV ) :- deriv(U,X,DU), deriv(V,X,DV). deriv(U/V, X, (DU*V-U*DV)/V^s(s(0))) :- deriv(U,X,DU), deriv(V,X,DV). deriv(U^s(N), X, s(N)*U^N*DU ) :- deriv(U,X,DU), nat_num(N). deriv(log(U), X, DU/U ) :- deriv(U,X,DU). pconstant(X) :- nat_num(X). pconstant(a). pconstant(b). nat_num(0). nat_num(s(X)) :- nat_num(X).Every clause is a differentiation rule copied out of a textbook, with the pattern in the head doing the work of deciding which rule applies. There is no dispatch code, no expression parser — the term is the parse tree.
?- deriv(x*x, x, D).Expected answer:
D = s(0)*x+x*s(0) ?The product rule, applied correctly — and left unsimplified, which is the honest output of these rules. Simplifying it is a separate program, and a good exercise.
?- deriv(log(x), x, D).Expected answer:
D = s(0)/x ?
:- module(_,_,[sr/bfall]). path(X,Y) :- edge(X,Y). path(X,Y) :- edge(X,Z), path(Z,Y). edge(a,b). edge(a,c). edge(b,d). edge(c,d). edge(d,e). edge(d,h).
?- path(a, e).Expected answer:
yes
Detecting a cycle — circuit :- path(A,A). — is the natural next question, and it is what the exercises take up.
:- module(_,_,[sr/bfall]). accept(S) :- initial(Q), accept_from(S, Q). accept_from([], Q) :- final(Q). accept_from([C|Cs], Q) :- delta(Q, C, NewQ), accept_from(Cs, NewQ). initial(q0). final(q0). delta(q0, a, q1). delta(q1, b, q0). delta(q1, b, q1).Deterministic finite automata are no trouble in any language. Non-deterministic ones are more awkward, because from some state two transitions leave on the same input and you do not know which to take. Except here — because in logic programming non-determinism is native.
Two things are being represented, and it is worth keeping them apart:
Where exactly is the non-determinism? In the call to delta/3. Running forwards, Q and C are given and NewQ comes back — but in state q1 reading b there are two transitions, one staying in q1 and one returning to q0. Take the first; if something fails later, backtrack and take the other. Nothing had to be programmed for that: it is what having several clauses for a predicate means.
?- accept([a,b]).Expected answer:
yes
?- accept([a,b,b,b]).Expected answer:
yes
?- accept([a,a]).Expected answer:
noNote that delta/3 has two clauses for delta(q1, b, _), so this automaton is non-deterministic — and nothing had to be done about that. The search that Prolog does anyway is exactly the search a non-deterministic automaton needs.
?- accept([A,B,C,D]). A = a, B = b, C = a, D = b ? ; A = a, B = b, C = b, D = b ? ; noThe first answer may surprise you: a b a b. Nothing said you could not pass through the initial state twice, so the language is really (a b+)*, not a b*. Ask for all accepted strings and you get [], [a,b], [a,b,b], [a,b,a,b], and onwards.
:- module(_,_,[sr/bfall]).
npda_accept(S) :-
npda_initial(Q),
npda_accept_from(S, Q, []).
npda_accept_from([], Q, []) :-
npda_final(Q).
npda_accept_from([C|Cs], Q, Stack) :-
npda_delta(Q, C, Stack, NewQ, NStack),
npda_accept_from(Cs, NewQ, NStack).
% A concrete automaton:
npda_initial(q0).
npda_final(q1).
npda_delta(q0, C, Stack, q0, [C|Stack]).
npda_delta(q0, C, Stack, q1, [C|Stack]).
npda_delta(q0,_C, Stack, q1, Stack ).
npda_delta(q1, C, [C|Stack], q1, Stack ).The stack starts empty, and the base case now demands that we finish in a final state and with the stack empty — everything pushed must have been popped.The slide asks: what sequence does it recognize? Rather than being told, run it.
?- npda_accept([a,b,b,a]).Expected answer:
yes
?- npda_accept([a,b,a,b]).Expected answer:
no
?- npda_accept([a,b,c,b,a]).Expected answer:
yesPalindromes. In q0 it pushes each symbol; the second and third transitions are the two ways of crossing the middle — the third skips one symbol, which is what allows an odd-length palindrome; and in q1 it pops, but only if the symbol read matches the top of the stack. That last clause, npda_delta(q1, C, [C|Stack], q1, Stack), uses the same variable C twice: reading and matching against the stack are one unification.
[a,a,a,a], [a,b,b,a], [a,c,c,a], [b,a,a,b], [b,b,b,b], [b,c,c,b], [c,a,a,c], [c,b,b,c], [c,c,c,c]
Three pegs, a stack of disks on the first, and the rules: move one disk at a time, never place a larger disk on a smaller one, and get them all from peg a to peg b using c.
With one disk you just move it. With two you cannot put the big one down first, so: small one to c, big one to b, small one on top. With three it takes seven moves. The legend says that when the monks of Hanoi finish moving their tower the world will end; apparently they are still at it.
What we want from the program is not just that it can be done, but the list of moves. Each move is a term move(A,B), meaning move the top disk of peg A to peg B.
The general predicate carries more than the number of disks: hanoi(N, Origin, Destination, Help, Moves). It needs Help because the pegs change roles — sometimes you are moving from a to b using c, and sometimes from a to c using b. Reasoning it out:
:- module(_,_,[]).
list([]).
list([_|Y]) :- list(Y).
list_append([], L, L) :- list(L).
list_append([X|Xs], Ys, [X|Zs]) :- list_append(Xs, Ys, Zs).
hanoi_moves(N, Moves) :- hanoi(N, a, b, c, Moves).
hanoi(s(0), A, B, _, [move(A,B)]).
hanoi(s(N), A, B, C, Moves) :-
hanoi(N, A, C, B, Moves1),
hanoi(N, C, B, A, Moves2),
list_append(Moves1, [move(A,B)|Moves2], Moves).The classic recursive puzzle, and the program is the statement of the solution: to move s(N) disks from A to B using C, move N from A to C, then move the bottom disk from A to B, then move the N from C to B. Note how the three pegs simply rotate between the recursive calls.
The two pictures are the base case and the general case: one disk moves straight across, and s(N) disks move as three smaller problems in sequence.
?- hanoi_moves(s(s(0)), M).Expected answer:
M = [move(a,c),move(a,b),move(c,b)] ?Three moves for two disks. And three disks:
?- hanoi_moves(s(s(s(0))), M).Expected answer:
M = [move(a,b),move(a,c),move(b,c),move(a,b),move(c,a),move(c,b),move(a,b)] ?Seven moves — as the picture on the slide shows, and as the formula 2ⁿ−1 says.
The program returns the moves as a list rather than printing them, so the plan is data: you can inspect it, count it with length/2, check it, or feed it to something else. That is worth contrasting with the usual imperative version, which prints as it goes and leaves you nothing.
Everything here has been pure: no cut, no assert, no arithmetic evaluation, no negation. The next deck adds those — and much of what it has to say is about what they cost.