☰

A Motivational Introduction

Course Notes (lecture version)


These are the notes accompanying the slides A Motivational Introduction (1_intro.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; those are here.

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!

  • Code needs to be loaded before running, by pressing on the question mark — code box should be checked ✔.
  • Under each program there are editable boxes with a right-pointing triangle (▶) that allow interaction with the code (once loaded!) by issuing queries.
  • Expected answers are printed in plain text underneath each query box, so you can read the notes straight through without running anything.
  • When available, pressing the northeast pointing arrow (↗) will load the code in a separate Prolog playground window.

What the whole area is about

This first set of slides is a motivational introduction. The idea is to justify a little why people do logic programming — or even programming and logic, and how the two are related.

That whole area of computer science, or of mathematical logic, is called computational logic, and it is all about the relation between logic and programming. A great many things you may have heard of live there: programming, algorithms, logic, lambda calculus, logic and AI, logic programming, knowledge representation, functional programming, constraints, the logic of programming, declarative programming. All of these are places where logic and programming, or logic and computer science, intertwine.

Logic and AI, for instance, has a lot to do with knowledge representation: representing knowledge and reasoning about it. Logic and programming — logic programming, functional programming, constraints — is about writing programs using logic. And verification is about proving that properties hold of programs, using logic.

Two halves. All of these divide roughly into two classes, and it matters which one this course is about.

The first is the logic of computation: applying logic to computational systems. An example is applying Boolean logic to logical circuits — those of you who come from the hardware world know that digital circuits are modeled with propositional logic, which gives you a mathematics in which to talk about the circuit. That is reasoning about computational objects with logic, and its classical application in computer science is verification: you have a property of a program and you want to prove it.

The second is different. It is not using logic to model computation or to reason about it — it is using logic to write computations, as a programming language. That is the part we are going to see: the direct use of logic as a programming tool.

Slides 2–5 — The code correctness problem

Before diving in, the whole picture. The basic motivation behind all of this is what is called the program correctness problem.

Consider a simple programming task:

"Compute the squares of the natural numbers that are less than or equal to 5"
In our age, AI is capable of programming anything. So ask it, and you get a program:

Now consider this alternative, written by a human programmer:

#include <stdio.h>
main() {
  int number, square;
  number = 0;
  while (number <= 5) {
    square = number * number;
    printf("%d\n", square);
    number = number + 1;
  }
}
Read it: it sets number to zero; while number is at most five, it multiplies number by itself, puts that in square, prints square, increments, and goes around again.

The two programs are semantically identical except for a small detail. One prints

1 4 9 16 25
and the other prints

0 1 4 9 16 25
Which one is right? Hold on to the question. We will be able to answer it precisely by the end of the deck, and the answer will not come from staring harder at either program.

Is this program correct? You do not have an answer to that, because you would immediately ask: correct with respect to what? What should it be correct with respect to? What is the specification?

So the first thing you need, in order to know whether a program is correct, is some kind of specification — some formalism that gives a description of the problem. What were we trying to solve? The program certainly does things, but there is no description of what is expected. There might be a comment, perhaps; there isn't one here.

So, two things are needed. First, we have to know what we want the program to do — that is the specification. Second, we want to be able to reason about whether the program corresponds to it. That second thing is the correctness problem, the proof of correctness.

It is not easy to determine correctness. Test it? How many tests? And what are the correct outputs anyway — was 0 an expected output? When you hand a program to a computer and it gives you an answer, is that answer what you expect? That is not easy to know.

Why this went from a minor worry to a major one. At the beginning this was a small concern in computer science, and it has slowly become a very large one, in the sense that nobody wants programs to fail. Initially it was easy to justify only for critical applications: you do not want the program controlling the plane you are flying in to fail, and you certainly do not want the program in the pacemaker in your heart, giving impulses to your heart, to fail. Once deployed there is no debugging and no second chance.

But nowadays people expect programs to work well in completely ordinary applications too. Nobody likes the computer freezing, or the screens of death you used to get. Fortunately that happens less and less — and it happens less thanks to the application of the techniques we are going to discuss, thanks to the fact that correctness has become an important problem.

And because of society's digital transformation, practically all software is critical in practice even when it is not safety-critical: prestige, economic loss, e-commerce, blockchain, banking, online administration, social media.

Software should carry a warranty, like anything else. Programs are not different from other artifacts that humans develop — a screw, a car, anything where you want a warranty, some certification that the thing works and does not do what it is not expected to. This is also expected of software, among other reasons because most objects you buy nowadays, from a cell phone to a car, are full of software. In fact most of the functionality of devices comes from software, not from the hardware. So the device will not work properly if the software does not, and the software has to be certified too.

And consider what certification means elsewhere. When you buy something as simple as a screw, it is certified to many standards: the metal it is made from is made to standards, it is certified to have certain tensile properties and resistance to heat, the thread conforms to a standard. Everything conforms to standards. Software also has to conform to standards.

So this is the problem being addressed. How do you manage to have components, parts of software, with assured quality — such that you are able to give a warranty about the software? Can I get software with some kind of warranty, so that even though I do not trust the person who is sending me the software, I can still trust the software, because there is a warranty or a proof attached? How can I trust software that comes from an untrusted party? These are very important questions, and they are all the program correctness problem.

Slide 6 — Natural language

Start with the obvious candidate for writing that specification:

"Compute the squares of the natural numbers which are less than or equal to 5."
It would seem ideal at first sight. But it is verbose, vague, ambiguous, and it needs context — a great deal of assumed information the reader is expected to supply. Philosophers and mathematicians pointed all this out a long time ago.

Notice that our sentence has already failed us once. It is exactly this sentence that leaves open whether 0 belongs in the output.

So a more suitable formalism is needed, for two distinct jobs: to provide specifications, that is, to describe problems; and to reason about the correctness of programs, that is, of their implementations.

Slide 7 — Logic

Logic is a means of clarifying and formalizing the human thought process.

Classical logic:

Aristotle likes cookies, and Plato is a friend of anyone who likes cookies

implies that

Plato is a friend of Aristotle

Symbolic logic is a shorthand for classical logic — plus a great many useful results:

a₁: likes(aristotle, cookies)
a₂: ∀X likes(X, cookies) → friend(plato, X)
t₁: friend(plato, aristotle)
T[a₁, a₂] ⊢ t₁
Which raises the two questions that drive the rest of the deck:

  • Can logic be used to represent problems — to write specifications?
  • And even, perhaps, to solve them?

Slide 8 — Using logic

For expressing specifications and reasoning about program correctness we need three things:

  • a specification language — for example assertions about input and output, modeling, and so on;
  • a semantics for the programming language — models, axiomatic, denotational, fixpoint;
  • proofs: verification of program correctness — for all possible inputs.
That last phrase is the whole difference between verification and testing, and the slide makes the point with two pictures side by side.

Trial and error?

You run the program on some inputs and look at what comes out. The question mark at the bottom is the point: you learn about the inputs you tried, and nothing about the ones you did not.

Formal proof!

Here the specification and the semantics of the language both feed a proof, and the proof answers yes or no — about all inputs at once. That is the whole reason logic is worth the trouble.

The lecture is careful to say what it is not going to do: writing program semantics is a subject of its own — "we'll have a whole course on that, and it's super fun, but it's not our subject." What follows is the specification half.

Building a specification

Slides 9–12 — Generating squares: building a specification

"Compute the squares of the natural numbers that are less than or equal to 5"
If we want to say that we generate the squares of the natural numbers, we first have to say what the natural numbers are. So let us formalize the problem starting from scratch — as a game, to specify things fully.

Peano numbers

Peano notation is a way to represent numbers axiomatically: to define mathematically what numbers are — to invent the numbers.

Start from the most basic numbering system in the world, the one where one is |, two is ||, three is |||. That is the most primitive numbering system there is — sticks, marks put one after another — and it is really Peano arithmetic already.

In mathematical logic you first have to invent a symbol for zero. Then one is represented by a zero followed by a stick, written s(0); two by two sticks, s(s(0)); three by three, s(s(s(0))).

0 ≡ 0     1 ≡ s(0)     2 ≡ s(s(0))     3 ≡ s(s(s(0)))     ...
0 ≡ 0     1 ≡ |        2 ≡ ||          3 ≡ |||            ...
This is just a structure — a piece of mathematical structure, a term in first-order logic that represents the three. And for it you need only one constructor, s, and a zero symbol. With s and 0 you can construct all the numbers. It is not the most efficient representation, but it is the most natural.

Defining the naturals — and why the "et cetera" is the whole problem

Now, how do you write that in mathematics? So far we have only drawn arrows and talked; it has to be written down. Here is how. We say nat is true of zero. And also — remember, ∧ is conjunction — that one, s(0), is a natural number. And also two. And so on:

nat(0) ∧ nat(s(0)) ∧ nat(s(s(0))) ∧ ...
But at some point you are going to write something like "et cetera". Why? Because you cannot write the infinite list. You cannot write all the infinite numbers down.

Which is where Peano's great idea comes in. Instead, write:

nat(0) ∧ ∀X ( nat(X) → nat(s(X)) )

Zero is a natural number, and for all X, if X is a natural number then s(X) is also a natural number. Those are the axioms of the natural numbers. In these two rules is contained the infinity of the natural numbers. All of them are defined there.

Facts, rules, and the model

That distinction is worth naming, because it recurs for the rest of the course. nat(0) is a fact — something without an implication. The second is a rule.

Facts are obvious: zero is natural, fine. The rule is not obvious, because in that one rule an infinite number of facts is contained. Every fact you would have written in the infinite conjunction — that zero is natural, that one is, that two is — is contained in the rule and follows from it.

The set of all those facts is the model of the two axioms: the infinite set of facts that they imply.

Less or equal

Now the ordering. The technique is the same, and it is worth watching, because it is the technique for everything that follows: write out the table of facts you want to be true, then find the generalizations.

le(0,0)              le(0,s(0))              le(0,s(s(0)))              ...
le(s(0),s(0))        le(s(0),s(s(0)))        le(s(0),s(s(s(0))))        ...
le(s(s(0)),s(s(0)))  le(s(s(0)),s(s(s(0))))  le(s(s(0)),s(s(s(s(0)))))  ...
...
The whole first row says "0 is less or equal than any natural":

∀X ( nat(X) → le(0, X) )

To generalize vertically, look down any column: if x ≤ y then x+1 ≤ y+1, that is

∀X ∀Y ( le(X,Y) → le(s(X), s(Y)) )

Two formulas, and the infinite table is captured — one for the base row, one for the step that generates each row from the one above.

Squares, which need multiplication, which needs addition

"The square of X is X times X":

∀X ∀Y ( nat(X) ∧ nat(Y) ∧ mult(X,X,Y) → nat_square(X,Y) )

which needs times, and "X times Y is adding Y to itself X times", so it needs add as well.

Addition, by exactly the same recipe. Write the table, generalize the first row to 0 + x = x, generalize the columns to if x + y = z then (x+1) + y = z+1:

∀X ( nat(X) → add(0, X, X) ) ∧ ∀X ∀Y ∀Z ( add(X,Y,Z) → add(s(X), Y, s(Z)) )

Multiplication goes the same way, with addition doing the work in the step.

No need to despair at the length of all this: in practice everything so far lives in libraries. What matters is that it can be written down completely, in logic, with no appeal to anyone's intuition about what a natural number is.

And now the specification itself

With the vocabulary defined we can write a formal specification of the imperative program — the conditions we want its outputs to meet.

  • Precondition (empty): true
  • Postcondition:
∀X ( output(X) ← ( ∃Y nat(Y) ∧ le(Y, s(s(s(s(s(0)))))) ∧ nat_square(Y,X) ) )

In words: X is an output for the program if there exists a natural number Y such that X is the square of Y and Y is less than or equal to 5.

Now read that against the question left open on slide 5. Is 0 an output? nat(0) holds, le(0,5) holds, nat_square(0,0) holds — so yes, on this specification 0 is an output, and the C program is the correct one. The point is not which answer is right. The point is that the question now has an answer, one that can be checked mechanically. That is what a specification buys you.

Slide 13 — An alternative use of logic?

So logic lets us represent and specify problems. But this course is not about that process — although we will do some of it along the way.

Go back to the two halves. One is applying logic to computation; the other is using logic to program. What we have been doing in the box above is using logic to prove the correctness of computations. What we actually want to do in this course is different: to see whether we can improve the process of writing programs, with logic. Can we use logic not only to specify — to represent the problem — but also to implement the solution?

This is where the deck turns, and the lecture is careful about why the question is not idle.

All real programming languages are Turing complete and so have the same theoretical expressive power. But that means a program can be written in any of them — not that it is equally compact or equally easy to write in one as in another. It can be done; it is not equally easy. So we want to improve the process, and for that the programming language matters a great deal.

Some relevant context: this was the 1960s, and great techniques and results were being obtained for automated theorem proving.

Slide 14 — From specification to computation: Green's dream

To answer the question we have to go back in time, to advances of the 1950s and 60s. This slide is Green's dream — Cordell Green, who came up with the idea.

Suppose we had a mechanical proof method, a deduction procedure: something that can make proofs automatically. This was the heyday of automatic theorem proving and resolution, when these methods were being invented and starting to become practical. Green said: if we had that, then we could look at problem solving and computing and programming in a completely different way.

  1. Program, once and for all, the deduction procedure into the computer. Put one program in the machine, a deduction system, something that can prove theorems. That is the program that runs.
  2. Then, given a problem:
    • (a) find a representation of it — the specification. For our squares, that would be the logic we just wrote. Give the specification to the computer.
    • (b) ask questions, and always get correct answers.
The computer is not really a computer any more at that point; it is an automated deduction system. You have to write that one program once. But if you have done it, and it works properly — in the sense that if something is a theorem it will say so, and give answers —

you never wrote the program.

You only wrote the specification, and you make the specification run. You get results directly from the specification. So you would not need a correctness proof at all. Of course the results are correct: they come from the specification. There was never a program in between for them to be wrong about.

Now, this was a dream: I have a problem, I write the specification, I throw it into the computer, I ask questions, and all the answers are correct. To what extent is the dream doable? At the time they were seeing the tip of the iceberg — that automated theorem provers were possible, and that this could be done at least in some cases. So the real question is: when can we do it? That is what the rest of the deck is about.

Slide 15 — Computing with our specification

Suppose we had written all of that in logic — naturals, less-or-equal, addition, multiplication, squares — and suppose we had an automatic theorem prover.

What a theorem prover is supposed to be, for our purposes: you give it a specification, a piece of logic, and then you ask a question — is this true, is this false, or for which values is it true — and it answers correctly. If it is a complete deduction procedure, it always answers. That is what we are assuming.

Then the following becomes possible.

If we ask We should obtain
nat(s(0)) ?yes
∃X add(s(0),s(s(0)),X) ?X = s(s(s(0)))
∃X add(s(0),X,s(s(s(0)))) ?X = s(s(0))
∃X nat(X) ?X = 0 ∨ X = s(0) ∨ X = s(s(0)) ∨ ...
∃X ∃Y add(X,Y,s(0)) ?(X = 0 ∧ Y = s(0)) ∨ (X = s(0) ∧ Y = 0)
∃X nat_square(s(s(0)),X) ?X = s(s(s(s(0))))
∃X nat_square(X,s(s(s(s(0))))) ?X = s(s(0))
∃X ∃Y nat_square(X,Y) ?(X = 0 ∧ Y = 0) ∨ (X = s(0) ∧ Y = s(0)) ∨ (X = s(s(0)) ∧ Y = s(s(s(s(0))))) ∨ ...
∃X output(X) ?X = 0 ∨ X = s(0) ∨ X = s⁴(0) ∨ X = s⁹(0) ∨ X = s¹⁶(0) ∨ X = s²⁵(0)

Go down that table slowly, because each row is doing something different, and together they are the argument of the whole deck.

Row 1: answering something we never wrote. Is one a natural number? Yes. But understand what has happened: I never wrote that one is a natural number. I wrote that zero is, and then I wrote a rule. So what I am asking is a consequence — something in the model, deducible from the two axioms. In this case the deduction is easy: does s(0) match nat(0)? No. Does it match the rule? Yes, if X is 0 — and nat(0) is a fact. So one is a natural number. The system should be able to say yes to something I did not write, because it is a logical consequence of what I did write.

Rows 2 and 3: addition, and why the prover must be constructive. I defined addition with a base case and a general case. In neither of them does it say that 1 + 2 is 3. But it is a logical consequence — it is in the model, in the table we abstracted. So the system should deduce it and produce it as output.

Which tells us something about what kind of prover we want: a constructive one. Not a prover that merely says yes or no, but one that says yes, and here are the values. When I write ∃X in the query, the query is a theorem I want proven — there exists an X which is the result of adding 1 and 2 — and I do not just want to be told that such an X exists. I want to be told that an example of that X is 3.

Row 3 is where it starts to pay. Is there an X which, added to 1, gives 3? Yes, and it is 2. So —

If I write addition, I do not have to write subtraction.

Subtraction is a derivative of addition; it is just putting the arguments differently. It need not be in my axioms at all.

Rows 4 and 5: several answers, and infinitely many. Ask whether there exists a natural number, and the system says: yes — in fact infinitely many. X = 0, X = s(0), X = s(s(0)), and so on, producing them until you stop it. All of them are in the model; all are deducible from the axioms.

Row 5 asks which numbers add to 1, and there are exactly two answers: X = 0, Y = s(0) and X = s(0), Y = 0. Two answers, each giving values for several variables at once.

Rows 6, 7 and 8: squares, and square roots for free. What is the square of 2? Multiply it by itself; the deduction gives 4. And then, exactly as with subtraction, you can also ask: is there a number which, squared, gives 4? Yes — 2. The system is doing square roots, and nobody wrote a square root.

Row 9: the problem we started with. ∃X output(X) — the postcondition of slide 12, asked as a question — yields 0, 1, 4, 9, 16, 25.

What we have achieved

If we had an automatic deduction procedure, we would have managed to avoid programming.

Go back to the picture. In the traditional model we write the specs, write a program, and then prove that the program implements the specs. Here: I represent the problem — only the specification — I give it to the deduction system, and I run it. I ask questions and get the answers I want. On this model, writing the specification is sufficient.

That would be great. But of course you know that although this was invented, and logic programming exists, people are still writing Java and people are still writing C — so obviously it is not as simple as that.

It can get very close, though, and getting close is exactly what this course is about.

Which logic, and what it costs

Slides 16–17 — Which logic should we use, and the tradeoffs

Having argued for representing the specification in logic, and having glimpsed a new view of computing, new questions arrive — and they are not independent of each other.

Does such a proof procedure actually exist? There are many candidates: truth tables, natural deduction, resolution, Prawitz/Bibel, tableaux, bottom-up fixpoint, rewriting, narrowing.

And which logic can we use? The more expressive the logic, the harder it is to have a mechanical procedure for it. That tension is the whole subject of the next two slides.

We would like to maximize expressive power. Compare propositions with first-order formulas. Propositional logic is useful:

"spot is a dog"     p
"dogs have tails"   q
but limited: we can say that p ∧ q is true and still not conclude that Spot has a tail — because nothing in p and q connects the dog in the first to the dogs in the second. They are opaque symbols. Predicate logic extends the expressive power:

dog(spot)
∀X dog(X) → has_tail(X)
and now deduction gives us has_tail(spot).

But expressive power is only half of what we need. We also need an effective mechanical proof procedure for whatever logic we choose. Hence the comparison.

Slides 18–19 — Comparison of logics

Propositional logic — p, "spot is a dog":

  • + decidable — roughly, "we can prove everything to be true or false"
  • + practical deduction mechanisms exist, for example truth tables
  • − limited expressive power — not Turing complete; it cannot express all programs
  • → very useful for circuit design, answer-set programming, and similar
First-order predicate logic — ∀X dog(X) → has_tail(X):

  • + good expressive power — Turing complete, we can express all programs
  • + a practical deduction mechanism — resolution
  • +/− semi-decidable — "we can always prove valid things in finite steps, but there is no guarantee of termination when something is not valid"
That last line is the crux, and it is the price of admission for the expressive power. It is why programs can loop.

Higher-order predicate logic — X(spot), "X is some relationship for spot":

  • + great expressive power
  • − undecidable
  • − no general deduction mechanisms
  • → but there are interesting subsets: higher-order logic programming, functional-logic programming
And an interesting case, the λ-calculus: similar to predicate logic in its results, and it allows higher order.

So first-order predicate logic is the sweet spot — enough power to express any program, and a proof procedure that actually works. That is the choice this course rests on.

Slides 20–21 — Generating squares by SLD-resolution

(These two slides are for readers who have already seen resolution; skip them otherwise. The lecture skips them, having covered resolution elsewhere.)

Code the problem as definite (Horn) clauses:

nat(0)
¬nat(X) ∨ nat(s(X))
¬nat(X) ∨ add(0, X, X)
¬add(X,Y,Z) ∨ add(s(X),Y,s(Z))
¬nat(X) ∨ mult(0, X, 0)
¬mult(X,Y,W) ∨ ¬add(W,Y,Z) ∨ mult(s(X),Y,Z)
¬nat(X) ∨ ¬nat(Y) ∨ ¬mult(X,X,Y) ∨ nat_square(X,Y)
Query nat(s(0)). To refute, add ¬nat(s(0)):

resolve with unifier giving
¬nat(s(0)) ¬nat(X) ∨ nat(s(X)) {X/0}¬nat(0)
¬nat(0) nat(0) {}□

Answer: yes.

Query ∃X ∃Y add(X,Y,s(0)). To refute, add ¬add(X,Y,s(0)):

resolve with unifier giving
¬add(X,Y,s(0)) ¬nat(X) ∨ add(0,X,X) {X=0, Y=s(0)}¬nat(s(0))

and ¬nat(s(0)) resolves as before. Answer: X = 0, Y = s(0). Taking the other clause instead gives the alternative solution, X = s(0), Y = 0.

This is the mechanical proof method Green's dream assumed, made concrete — and it is the one that actually works.

The dream, delivered

Slides 22–23 — Generating squares in a practical system

With resolution, things actually work. The questions we were asking before about our example — is one a natural number, what are the natural numbers — work with resolution, in a real system.

Translating the logic into Prolog

We take the same specification and write it in Prolog. Actually, not quite in Prolog — in pure logic programming. Ciao can be put in pure logic programming mode, which is what we want for the first part of the course.

The translation is almost nothing. We put the arrows backwards. Instead of writing "∀X, nat(X) implies nat(s(X))", we write it the other way round: the :- is the arrow, pointing right to left. So nat(s(X)) :- nat(X) — read :- as "if" — says s(X) is a natural number if X is a natural number.

The same for the rest. le(0,X) :- nat(X) says 0 is less or equal than any X provided X is a natural number. le(s(X),s(Y)) :- le(X,Y) you can read either way: X+1 ≤ Y+1 if X ≤ Y, or if X ≤ Y then X+1 ≤ Y+1. The commas are the ∧s, and :- is the arrow backwards.

:- module(_,_,[sr/bfall]).

% A natural is 0 or s(X) where X is a natural
nat(0).
nat(s(X)) :- nat(X).

% We define less or equal
le(0,X) :- nat(X).
le(s(X),s(Y)) :- le(X,Y).

% Definition of addition
add(0,Y,Y) :- nat(Y).
add(s(X),Y,s(Z)) :- add(X,Y,Z).

% Definition of multiplication
mult(0,Y,0) :- nat(Y).
mult(s(X),Y,Z) :- add(W,Y,Z), mult(X,Y,W).

% Definition of the square of a natural
nat_square(X,Y) :- nat(X), nat(Y), mult(X,X,Y).

% What we want the output of our program to be
output(X) :- nat(Y), le(Y,s(s(s(s(s(0)))))), nat_square(Y,X).
sr/bfall selects a breadth-first search rule instead of Prolog's usual depth-first one. It is needed here because mult/3 calls add/3 before recursing, and under depth-first search that order runs off down an infinite branch. Breadth-first is slower but complete — the honest way to run a specification written with no thought for execution order.

In the lecture this is passed over quickly ("we'll explain what that is; anyway, this thing here, but I'm not explaining that now"), and it is worth flagging as the first appearance of the theme that occupies most of the course: the logic is fine, and the control needs attention.

Where Green's dream actually is, in this picture

Before running anything, notice the correspondence, because this is the dream made literal:

  • the Ciao system running in the computer is the deduction procedure, programmed once and for all;
  • the program above is the specification, the logic that says what addition is;
  • the queries below are the questions.

Loading it

One detail from the demo worth keeping, because it is the commonest beginner's confusion. Asked nat(0) before loading the program, the system answers no — not because zero fails to be a natural number, but because nothing has been defined yet. The program has to be loaded first.

The demo

Is zero a natural number? That is written down explicitly, so of course:

?- nat(0).
Expected answer:

yes
Now the more interesting one. This I did not say. I said zero was a natural number, and then gave a rule from which all the others can be deduced:

?- nat(s(0)).
Expected answer:

yes
The system is doing logical deductions to answer that.

Addition, forwards:

?- add(s(0), s(s(0)), X).
Expected answer:

X = s(s(s(0))) ?
And backwards — what is 3 − 1? Or, put as a question about addition: is there an X which, added to 1, gives 3?

?- add(s(0), X, s(s(s(0)))).
Expected answer:

X = s(s(0)) ?
Subtraction, from a definition of addition.

Several answers. Ask what the naturals are. Typing ; asks for another answer:

?- nat(X).
Expected answer:

X = 0 ? ;
X = s(0) ? ;
X = s(s(0)) ? ;
X = s(s(s(0))) ? ;
This is an infinite set of solutions, and the system computes them incrementally so you can stop at any point — press Enter instead of ; and it stops.

All the ways of adding to 1:

?- add(X, Y, s(0)).
Expected answer:

X = 0,    Y = s(0) ? ;
X = s(0), Y = 0    ? ;
no
Both answers, and then no — meaning there are no more. Those are the only two possibilities.

Squares:

?- nat_square(s(s(0)), X).
Expected answer:

X = s(s(s(s(0)))) ?
And square roots — is there a number which, squared, gives 4?

?- nat_square(X, s(s(s(s(0))))).
Expected answer:

X = s(s(0)) ?
Why does that work? Because what we wrote was the definition of squares, on top of the definition of multiplication, on top of the logical definition of addition. Everything we are asking here is the question is this a theorem in the theory we wrote? — and it is. And because the deduction procedure is constructive, it does not merely say yes; it gives values for the variables. So we get examples.

And with both unbound, the squares as pairs:

?- nat_square(X, Y).
Expected answer:

X = 0,          Y = 0 ? ;
X = s(0),       Y = s(0) ? ;
X = s(s(0)),    Y = s(s(s(s(0)))) ? ;
X = s(s(s(0))), Y = s(s(s(s(s(s(s(s(s(0))))))))) ? ;
And finally, the specification itself, run as a program:

?- output(X).
Expected answer:

X = 0 ? ;
X = s(0) ? ;
X = s(s(s(s(0)))) ? ;
X = s(s(s(s(s(s(s(s(s(0))))))))) ? ;
0, 1, 4, 9 — the squares. Nobody wrote a program to compute squares. What was written is the postcondition from slide 12, transcribed, and it ran.

This is a materialization of Green's dream. This is exactly that, working.

And notice the first answer: 0. The specification says nat(Y), le(Y,5), and 0 is a natural not greater than 5, so 0 is an output. That settles the question left open on slide 5 — the human C program was right and the AI's was wrong, and we can now say why, with reference to something other than taste.

It is worth being clear about how far this goes. It works because the specification happens to be in a form — Horn clauses — for which a good proof procedure exists, and because we accepted breadth-first search. Getting acceptable performance out of this idea, while keeping as much of the declarative reading as possible, is the subject of the rest of the course. And "we're programming, we're writing programs now" is exactly the point: this is not a demonstration of logic, it is programming.

Exercises

A real logic program! — opens in the Ciao playground, the same link as the button on the slide.

Where it went

Slides 24–25 — Applications, and why (constraint) logic programming

Many applications, often in combination with other languages:

  • natural language processing
  • scheduling and optimization problems
  • many AI-related problems; (multi-)agent programming
  • heterogeneous data integration
  • program analyzers and verifiers
  • LP as the logical, reasoning and explanation component of LLMs
Some concrete examples: the IBM Watson system has important parts written in Prolog; Clarissa, a NASA voice user interface for browsing ISS procedures. The lecture adds one more that is not on the slide, and it is a good one: the Java abstract machine is specified in Prolog — look at the specification and you will find Prolog rules, because the people who designed Java were Prolog people.

Why, though? A quote from the Watson developers is more persuasive than any argument from elegance, because this is a team that had already built the alternative:

"Prior to our decision to use Prolog for this task, we had implemented custom pattern-matching frameworks over parsers. These frameworks ended up replicating some of the features of Prolog but lacked the full feature set of Prolog or the efficiency of a good Prolog implementation. Using Prolog for this task has significantly improved our productivity in developing new pattern-matching rules and has delivered the execution efficiency necessary to be competitive in a Jeopardy! game."

— Adam Lally et al., Question analysis: How Watson reads a clue, IBM J. Res. Dev. 56(3)

Watson is the question-answering system from IBM's DeepQA project. It won first place and $1 million on Jeopardy! against the champions Brad Rutter and Ken Jennings — the first system to beat humans at a genuinely difficult game, not chess but one involving common knowledge and wordplay.

Note both halves of the quote. They had reinvented Prolog badly — and then found the real thing was also faster. The productivity argument and the efficiency argument point the same way, which is not what people usually expect of a declarative language.

Slides 26–27 — A very brief history

The 1960s: the origins.

  • Green: programming as problem solving — the dream of slide 14.
  • Robinson: resolution, the deduction procedure.
These were really the basis. Logic programming was only possible because these things were around, because they had been invented.

The 1970s: the procedural interpretation.

Two people, Bob Kowalski and Alain Colmerauer, came up with the idea of giving a procedural interpretation to Horn clause logic.

That is, to look at something like the definition of multiplication — two lines of logic — and say: I see a procedure. I see something that runs. I see an algorithm in there. We will learn to do exactly that: to look at such a definition and make it run, and watch how it runs. In the end you just run a debugger, and it is not that different from programming in C. But there is a big mental step from looking at two phrases of logic to deducing that what you are looking at is a set of instructions.

The trick, when you see a clause A :- B₁, ..., Bₙ, is to read it as: to solve A, to execute A, execute B₁ and then B₂ and then ... and then Bₙ. Look at the phrases of logic as procedures.

Which gives Kowalski's equation:

Algorithm = logic + control.

Look at a clause. It is a piece of logic that states a relation: A is true if B is true, and so on. But if you read it left to right, that is the control; if you give it an order, the same thing is also a set of instructions. That duality is what makes it interesting — and beautiful.

Everything in this course that looks like a performance trick is really a change of control leaving the logic alone; the sr/bfall on slide 22 was the first example.

And the rest is history. Colmerauer, in France, built the first practical system and gave it the name Prolog, from Programmation et Logique. The interpreter was written in Fortran.

Then David Warren wrote the first Prolog compiler. Up to that point this was a great idea that ran extremely slowly. Warren's compiler was itself written in Prolog, which was astonishing — nobody thought you could write a compiler in such a language — and it turned out to be as fast as Lisp, at the time the fastest and most mature symbolic language. That was a small revolution, because it made the whole thing practical.

The 1980s and 1990s.

  • Numerous commercial Prolog implementations and programming books, using the de facto standard, the Edinburgh Prolog family.
  • Leading in 1995 to the ISO Prolog standard.
  • Parallel and concurrent logic programming systems.
  • Constraint Logic Programming (CLP) — a major extension that opened new areas and new communities: commercial CLP systems with fielded applications, and concurrent constraint programming systems.
2000 to today.

  • Many further extensions: full higher order, support for types and modes, concurrency, distribution, objects, functional syntax.
  • Highly optimizing compilers, automatic parallelism, automatic verification and debugging, advanced environments.
  • Many applications, as listed above.
  • Increased current interest: explainable AI, and combination with ML and LLMs.
Which closes the circle the deck opened with. Slide 3 asked an LLM to write the squares program and got something whose correctness we could not judge. Slide 27 lists logic programming as the reasoning and explanation component of such systems. The technology that could not tell us whether 0 belongs in the output is the one that now wants a component that can.