Part IV — Logic, reasoning and planning · Chapter 8

Logic, computation and Prolog

~50 min read6 interactive widgets

In this chapter

  1. Logic, and logic in computer science
  2. Two notions of truth
  3. Computation and deduction
  4. Judgements, deductions, proof trees
  5. Proof search: forward and backward
  6. The origins of logic programming
  7. Terms, atoms, formulae
  8. Clauses, Horn clauses, logic programs
  9. SLD resolution
  10. Prolog: syntax and execution
  11. Numbers, and recursion
  12. Lists
  13. Check your understanding
Editor’s note

This chapter merges two decks: M8 — Logic & Computation, the theory lecture, and C3 — Prolog: Fast Track, the lab case study. They are the two halves of one arc: M8 builds computational logic up to SLD resolution and stops at the door of a language; C3 walks through that door and writes programs. Sections 1 to 9 are M8, sections 10 to 12 are C3, and the section on the parent/grandparent example belongs to both, since both decks use it.

1. Logic, and logic in computer science

Logic is the study of correct reasoning, especially as it involves the drawing of inferences. It studies the way we draw conclusions and express ourselves, and deals with how to formalise that [Metakides and Nerode, 1996]. It dates back to Aristotle, for whom logic is the instrument for human knowledge [De Rijk, 2002].

Computational logic (CL) is logic in computer science [Lloyd, 1990]. The deck’s formulation of its scope is worth memorising because it is a three-part list, not a slogan: it concerns all uses of logic in computer science — to compute, to represent computation, and to reason about computation. It is rooted in logic, applied mathematics, artificial intelligence and computer science.

A short history

After a long lack of interest, at the end of the seventeenth century Gottfried Leibniz observed — quite in isolation — that if ideas are concepts like numbers, they should be representable and manipulable in the same way as numbers, clearly bridging logic and mathematics for the first time [Levesque, 2012], crediting Ramon Llull’s Ars Generalis (1308) as an inspiration.

A formalism for mathematics is typically credited to George Boole, from the middle of the nineteenth century [Boole, 2009]: along with Augustus De Morgan, they started extending logic to become a tool for the study of the foundations of mathematics. Logic finally regained centrality in Western thought with Gottlob Frege, who introduced the first formal language for logic and mathematics, to ground mathematical reasoning on a sound basis [Frege, 1971].

Then the crisis. Starting from a paradox in Cantor’s theory of numbers, Russell’s paradox — the set of all sets that do not belong to themselves — exposed the logical inconsistencies at the foundations of mathematics. This made clear that only a rigorous formalisation could lead to well-founded mathematical reasoning and theories. Around 1920 Hilbert’s programme aimed at addressing the issue, and the acme of those efforts was the Principia Mathematica [Whitehead and Russell, 1927], a complete logic formalisation of all mathematics treated as a whole axiomatic system — soon to be undermined by Gödel’s incompleteness theorems [Gödel, 1931].

2. Two notions of truth

The deck introduces the central distinction with two examples that are worth keeping side by side.

So there are two ways to set the truth of a sentence: by considering it as a sequence of symbols and manipulating them independently of their meaning; or by interpreting the symbols and considering the truth of the meaning of the sentence.

Alfred Tarski [Tarski and Tarski, 1994] formalised the notion of logical interpretation as the relation between the symbols (syntax) and the elements of the domain of discourse (semantics). And he defined logical entailment, or logical consequence, precisely: when a conclusion is true in every interpretation making all premises true, then the premises logically entail the conclusion.

From which the deck draws a chain of consequences that motivates everything after it. If a conclusion is true for any interpretation, it is true independently of the interpretation, hence independently of the semantics of symbols; and if it is true independently of the semantics of symbols, it would likely depend on symbols only; therefore the syntactic notion of truth is in principle connected with logic entailment.

Two worked examples

If p and q hold, does r hold too? Interpret p as “all men are mortal”, q as “Socrates is a man”, r as “Socrates is mortal”, and we know it does. Interpret p as “Charles drives a beautiful and fast red car”, q as “Max drives an unreliable blue car”, r as “Kimi drives a horrible and slow silver car”, and we do not know. Since there is at least one interpretation by which r is not true given p and q, r is possibly true, yet is not a logical consequence of p and q.

If p and p → q hold, does q hold too? Whichever our interpretation of p and q, q is always true given p and p → q. Since its truth value does not depend on the interpretation of symbols, q is a logical consequence of p and p → q.

After Tarski’s work, logic is typically discussed as a two-sided story [Levesque, 2012]: a syntactic side involving axioms and rules of inference — sometimes called a proof theory; and a semantic side involving interpretations and truth — sometimes called a model theory; with soundness and completeness theorems relating the two sides.

SYNTACTIC SIDE — proof theory SEMANTIC SIDE — model theory symbols manipulated independently of meaning axioms rules of inference derivations, proof trees H |- F (F is derivable) symbols interpreted over a domain of discourse interpretations truth in an interpretation models H |= F (F is entailed) soundness what is derived is entailed completeness what is entailed is derivable Tarski put the two sides in contact: if a conclusion is true in EVERY interpretation making the premises true, then its truth cannot depend on the interpretation, so it depends on the symbols only. Logic programming lives on the left-hand side and computes by manipulating symbols — and Kowalski showed the two readings coincide.
Plate 8.1 — The two sides of logic truth, and the two theorems that hold them together. Everything computational in this chapter happens on the left; everything meaningful is guaranteed by the arrows.

3. Computation and deduction

Pfenning’s formulation, quoted by the deck, is the pivot of the whole module:

To compute we start from a given expression and, according to a fixed set of rules (the program), generate a result. For example, 15 + 26 → (1+2+1)1 → (3+1)1 → 41.

To deduce we start from a conjecture and, according to a fixed set of rules (the axioms and inference rules), try to construct a proof of the conjecture. For example, we conjecture that an + bn ≠ cn for n > 2.

→ So computation is mechanical and requires no ingenuity, while deduction is a creative process. For example, proving that an + bn ≠ cn for n > 2 basically took 357 years of hard creative work.

Roughly, then: computation can be thought of as an automatic process and delegated to a machine, while deduction requires some form of intelligence. In restricted areas the two can be unified [Pfenning, 2007] — Boolean algebras, for instance. More generally, even if we follow a well-defined set of formal rules, we know that not everything we can reason about is mechanically computable, given the fundamental undecidability results.

Yet the two do meet, in both directions. Computation can be seen as a limited form of deduction, since it establishes theorems: 15 + 26 = 41 is both the result of a computation and a theorem of arithmetic. And deduction can be considered a form of computation if we fix a strategy for proof search, removing the guesswork — and the possibility of employing ingenuity — from the deductive process. That is, if we automate deduction. As Pfenning puts it, this latter idea is the foundation of logic programming: logic program computation proceeds by proof search according to a fixed strategy; by knowing what this strategy is, we can implement particular algorithms in logic, and execute the algorithms by proof search.

Where autonomy lives

Now the move that makes this a chapter of this course rather than a logic lecture. The possibility of choice, of deliberation, is a key issue in the relationship between computation and deduction. “Removing the guesswork” from the deductive process, fixing a strategy for proof search, means there is no longer space for choice. So autonomous choice and rational deliberation are required whenever deduction is required and computation cannot be implemented as an automatic technique. In the deck’s own words: whenever computation and deduction do not exactly match, there lies the space for autonomy.

And then the deck immediately objects to itself, which is why the passage is worth reading twice. Wait: autonomous computation is still computation; in principle, it cannot be used to bridge the gap between deduction and computation. True. The resolution: the relationship between deduction and computation is stated independently of the domain of discourse, hence independently of specific knowledge about the domain — which is precisely what mathematicians use when they try to prove theorems. So in order to instrument and exploit autonomous choice in the space between deduction and computation in non-trivial domains, knowledge about the specific domain is required, leaning onto the semantic side, and can be used to drive the choices whenever an automatic strategy does not exist — for instance, to determine which path to follow in the proof tree.

Key idea

This is the same discovery Chapter 6 recorded historically, arriving here by a formal route. There, the way out of the AI winter was that humans use extensive domain knowledge to cut down the search space. Here, domain knowledge is what an autonomous agent needs in order to choose in the gap between what is mechanically computable and what must be deduced. Two vocabularies, one insight: knowledge is what makes choice tractable.

4. Judgements, deductions, proof trees

A judgement is an object of knowledge — knowing a judgement comes from the evidence we have for it [Martin-Löf, 1996]. The most common judgement is A true: given a proposition A, A is true. Most of logic programming deals with the truth of propositions. Other judgements exist: A false, for true negation; A true at t, the subject of temporal logic; K knows A, the subject of epistemic logic.

The most interesting evidence is a proof. Let J, J1, ..., Jn be judgements and R an inference rule: a deduction is a proof that can be read equivalently as “if J1 and ... and Jn, then we can conclude J by virtue of rule R”, or “given J1 ... Jn, we can prove J by rule R”, or “J can be deduced or inferred from J1 ... Jn by means of rule R”. J1 ... Jn are the premises and J is the conclusion, written as a fraction with the premises above the line, the conclusion below, and R labelling the line.

If we know J1 ... Jn and the rule concluding J from them, we can infer J. How do we know, say, J1? We may know J1,1 ... J1,n and a rule R1 concluding J1 from them. Thus we may build an inference chain to prove J — if possible.

Given the structure of a deduction, the data structure covering all possible inference chains for a judgement J is a proof tree: whose root is the conclusion J; whose edges are deductions labelled by an inference rule; and where premises are child nodes and conclusions are parent nodes.

What are the leaves? Where do we start with our deductions? Is there any judgement that can be said to be true without proof? This is the role of axioms: an axiom is, in logic, an indemonstrable first principle, rule or maxim that has found general acceptance or is thought worthy of common acceptance, whether by virtue of a claim to intrinsic merit or on the basis of an appeal to self-evidence. Axioms can be used as premises in a proof tree with no need of proof.

5. Proof search: forward and backward

How do we explore the proof tree? Should we start from what we know and try to prove our conjecture, deduction after deduction? Or start from the conjecture, find the deductions that prove it, and try to prove the premises, recursively? Or build the tree in some other way and explore it with some other strategy?

StrategyHow it goesDirection in the tree
Forward chaining
forward-reasoning search
We start from what we know (initially, the axioms); we exploit inference rules to deduce new judgements (theorems); we add new evidence to the old one; recursively; until we obtain our conjectureFrom the leaves down to the root
Backward chaining
goal-directed search
We start from what we need to prove (initially, the conjecture); we exploit inference rules to find the judgements it depends upon; we find new judgements to prove; recursively; until we end up depending only on the axiomsFrom the root up to the leaves
J J1 Jn A A A A R1 Rn R1,1 R1,n Rn,1 Rn,n axioms: no proof needed the conjecture FORWARD leaves to root: from what we know to the conjecture BACKWARD root to leaves: from what we must prove down to the axioms
Plate 8.2 — The same proof tree, two ways to walk it. Prolog takes the vermilion route: resolution leads to backward chaining, from goal back to axioms. Chapter 10 will meet the identical pair again under the names progression and regression planning.

Would this even be a computation?

Both strategies seem to fit computation, but the deck raises four questions — would this be a deterministic computation? would it converge? would it actually terminate? could it be a parallel, concurrent or distributed computation? — and answers them preliminarily:

Start from what you know — initially the axioms — exploit inference rules to deduce new judgements, add the new evidence to the old, recursively, until the conjecture is obtained. It traverses the proof tree from the leaves down to the root. It is data-driven: you find out what follows from what you have.

Start from what you need to prove — initially the conjecture — use inference rules to find the judgements it depends upon, and recursively prove those, until you depend only on axioms. It traverses the tree from the root up to the leaves. It is goal-directed, and it is what SLD resolution does: resolution leads to backward chaining, from goal back to axioms.

A definite clause A ← B1, ..., Bm read declaratively says: A is true if B1, ..., Bm are true. This is a statement about the world, with no notion of execution in it. Aristotle’s sense of declarative is a sentence that can be said either true or false, so declarative programming means programming through true sentences declaring what to compute — the meaning.

The same clause read procedurally says: to prove A, prove B1, ..., Bm. Now it is a procedure declaration, and calling it means proving its subgoals. Procedural programming is programming through operational statements determining how to compute — the method. And the point of the whole chapter: for logic programs the two interpretations coincide [Kowalski, 1974], which is a property logic programming languages such as Prolog are the only ones to hold [Metakides and Nerode, 1996].

6. The origins of logic programming

The early history [Apt, 2005]: automatic deduction of theorems; first-order logic by Frege, Peano and Russell; computation as deduction by Gödel and Herbrand; and the resolution principle by Robinson [1965], along with unification.

Resolution allowed the proof of first-order theorems and made it possible to compute with logic — but not yet to see logic as a full computational framework. Something was still missing between computable logic and logic as a programming language.

What was missing was the procedural interpretation of Horn clauses. By defining logic programs as collections of Horn clauses, and by restricting Robinson’s principle accordingly, Kowalski showed how a logical implication could be amenable to both a declarative and a procedural interpretation [Kowalski, 1974], thus providing the foundations for a logic programming language. Prolog, by Colmerauer in Marseille, came along in 1973. As Colmerauer and Roussel later put it: there is no question that Prolog is essentially a theorem prover à la Robinson; our contribution was to transform that theorem prover into a programming language.

Three fundamental features

FeatureContent
termsComputing takes place over the domain of all terms defined over a “universal” alphabet
mguValues are assigned to variables by means of automatically-generated substitutions, called most general unifiers. These values may contain variables, called logical variables
backtrackingThe control is provided by a single mechanism: automatic backtracking

Two further properties are worth recording. Logic programs can be seen as executable specifications: the logic programmer is concerned with what to compute, while how to compute — the control — is delegated to the underlying logic programming machinery. Sometimes this could lead to inefficiency. And logic programming languages can be seen as formalisms for either executable code or knowledge representation, which makes them languages for artificial intelligence.

Finally, interactive programming: the model behind computation-as-deduction natively supports writing a logic program and then interacting with the logic machinery by means of multiple queries, or by asking for multiple solutions. Logic languages intrinsically support the interactive style of programming and computing — a feature useful in distributed systems too, supporting notions such as LPaaS, Logic Programming as a Service [Calegari et al., 2018].

7. Terms, atoms, formulae

Logic programming is ruled by different principles from other programming paradigms: atomic actions are equations between terms, executed by means of the unification process trying to solve them; unification assigns values to variables; and values can be arbitrary terms — in fact there is just one sort of variable, ranging over the set of all terms.

Terms

The definition is recursive:

Let X, Y be variables, a, b constants, f and g functors of arity 3 and 2. Then a, b, X, Y are proper terms; f(a,b,a) and g(X,Y) are proper terms; f(a, X, g(Y,b)) is a proper term. Variables and constants are atomic terms; terms built out of proper functors are structured terms. In f(a, X, g(Y,b)), f is the functor symbol of arity 3, and a, X, g(Y,b) are the three subterms.

Remarks that matter: the definition is recursive, leading to a recursive data structure — a tree. Terms are fundamental in mathematical logic and essential in computer science: they capture both arithmetic expressions and strings. No specific alphabet is assumed — a universal alphabet for all terms. And no meaning is a priori attached to symbols, in particular to functors: + is just a functor, not associated a priori with the plus sign of arithmetic. Hence: no types.

f a X g Y b functor of arity 3 constant variable functor of arity 2 variable constant TERM f(a, X, g(Y, b)) — a recursive data structure, hence a tree atomic terms: a, b, X, Y structured terms: f(a,b,a), g(X,Y), f(a,X,g(Y,b)) — and no types anywhere: + would be just another functor
Plate 8.3 — The deck’s own example term, redrawn. Once you see terms as trees, unification becomes what it really is: an attempt to make two trees identical by binding their variables.

From terms to propositions

Logic programs compute over the truth values of sentences, so how do we write sentences? We know how to denote the elements of the domain of discourse, not yet how to talk about them. Sentences in logic are typically called propositions.

Predicates do the job. If p is a predicate symbol of arity n and t1, ..., tn are terms, then p(t1, ..., tn) is an atom. Atoms represent elementary propositions in logic programming; if A is an atom, then A is a logic formula stating that A is true.

Negation makes it possible to deal with false propositions: if A is an atom, ¬A is a logic formula stating that A is false, and A, ¬A are literals. Literals can be combined through logical connectives: conjunction A ∧ B, disjunction A ∨ B, implication A → B, equivalence A ↔ B.

8. Clauses, Horn clauses, logic programs

A logic clause is a finite disjunction of literals [Console et al., 1997]. If A1, ..., An, B1, ..., Bm are atoms containing variables X1, ..., Xk, then

∀X1...Xk (A1 ∨ ... ∨ An ∨ ¬B1 ∨ ... ∨ ¬Bm)

is a logic clause, logically equivalent to

∀X1...Xk ((A1 ∨ ... ∨ An) ← (B1 ∧ ... ∧ Bm))

usually written simply as A1, ..., An ← B1, ..., Bm. A clausal normal form (CNF) is a conjunction of clauses.

Now the restriction that makes logic programming possible:

KindShapeConditionIn a logic program
definite clauseA ← B1, ..., Bmexactly one positive literal (n = 1)a rule
unitary clauseA ←one positive, no negative literal (n = 1, m = 0)a fact
definite goal← B1, ..., Bmno positive literal (n = 0)a goal
Horn clauseeither a definite clause or a definite goal (n = 1 or n = 0)

Hence: a logic program is a CNF of Horn clauses, that is, a conjunction of rules and facts (and goals).

For the exam

Two things from this section are asked constantly. First, what makes a clause a Horn clause: at most one positive literal — one for definite clauses, none for goals. Second, why the restriction matters: Robinson’s resolution principle works for general clauses and shows that it is possible to compute by contradiction whether a CNF entails a formula, but it does not provide a proof strategy for a full-fledged programming language; restricting to Horn clauses and recasting the principle accordingly gives SLD resolution, and with it the procedural interpretation that coincides with the declarative one.

9. SLD resolution

Robinson’s resolution principle works for general clauses: given a CNF H and a formula F, it shows that it is possible to compute (by contradiction) whether H logically entails F — however, it does not provide a proof strategy for a full-fledged logic programming language. Kowalski showed this could be obtained by restricting logic programs to CNFs of Horn clauses and recasting Robinson’s principle accordingly: the so-called SLD-resolution principle [Nilsson and Maluszynski, 1995].

Proving proceeds by contradiction. Robinson’s principle tries to prove a formula F false against a CNF H, succeeding if this fails — technically, proving that H ∪ ¬F is not satisfiable. Proving an atom G in logic programming amounts to proving ¬G against logic program P, that is, proving goal ← G on P. Computation in logic programming proceeds by proving goals, and resolution leads to backward chaining, from goal back to axioms.

SLD resolution, informally

To prove a goal G with respect to program P, the resolution principle proceeds according to the procedural interpretation. First we look for one clause A ← B1, ..., Bn in P whose head A unifies with G. If the most general unifier of G and A is θ, then the proof of G succeeds if we can further prove B1θ, ..., Bnθ — where Biθ represents the application of the mgu to Bi. The application of θ to the clause specialises it to the specific atom we need to prove, and resolution proceeds recursively with the proof of the subgoals. In general, the computational state of SLD resolution includes a (possibly empty) conjunction of atoms to be proven — the current goal.

How it ends, if it does:

The inference rule itself, in the deck’s notation:

← A1, ..., Ai-1, Ai, Ai+1, ..., Am        B0 ← B1, ..., Bn
------------------------------------------------------------------
     ← (A1, ..., Ai-1, B1, ..., Bn, Ai+1, ..., Am)θ

where A1...Am are atomic formulas and ← A1, ..., Am is the conjunction of subgoals to prove; B0 ← B1, ..., Bn is a definite clause in program P (n ≥ 0), suitably renamed with new and unique variable names to avoid name clashes; and there is an Ai unifying with B0 such that mgu(Ai, B0) = θ.

Two non-determinisms

SLD resolution is non-deterministic in two independent ways:

The choice does not affect correctness of the resolution, so we could choose non-deterministically. But how to exploit either or-non-determinism or and-non-determinism, or both, determines how the automatic resolution process explores the proof tree. And different computational models — sequential, parallel, concurrent — could be exploited: more clauses with a unifying head could be used for goal proof at the same time, either in parallel or concurrently.

The worked example

Both decks use the same program, so it is the natural place to see everything at once:

parent(joey, luca).
parent(joey, simone).
parent(lino, joey).
parent(mirella, joey).

grandparent(X, Z) :- parent(X, Y), parent(Y, Z).

Declaratively: four facts expressed by predicate parent/2 — four propositions considered true with no need of proof, our axioms; a possible interpretation is that joey is a parent of luca, just one of the many, even though the most intuitive for English speakers. One rule expressed by grandparent/2, short for ∀X,Y,Z: grandparent(X,Z) ← parent(X,Y), parent(Y,Z), meaning that the formula grandparent(X,Z) holds if both parent(X,Y) and parent(Y,Z) are true, whatever the values of X, Y, Z.

Procedurally: two procedures are defined, parent/2 and grandparent/2, and two kinds of call can be executed. To compute parent/2 we use the four facts, non-deterministically. To compute grandparent/2 we use the rule, first matching the rule head, then proceeding by calling the two subprocedures via the two subgoals of the form parent/2.

And the possible goals, with their answers:

GoalOutcome
grandparent(lino, luca)succeeds — one refutation, no computed substitution
grandparent(lino, joey)fails — no refutations
grandparent(lino, X)succeeds twice — X/luca, X/simone
grandparent(X, simone)succeeds twice — X/lino, X/mirella
grandparent(X, Y)succeeds four times — X/lino Y/luca; X/lino Y/simone; X/mirella Y/luca; X/mirella Y/simone
<- grandparent(lino, X) <- parent(lino,P), parent(P,X) <- parent(joey, X) <- [] empty goal: refutation clause: grandparent(G,N) :- parent(G,P), parent(P,N) mgu = {G/lino, N/X} clause: parent(lino, joey) mgu = {P/joey} — after two failed head matches clause: parent(joey, luca) mgu = {X/luca} on backtracking: the choicepoint at parent(joey, X) yields the second solution X/simone
Plate 8.4 — One SLD derivation for ?- grandparent(lino, X), ending in the empty goal. The dashed arrow is the choicepoint Prolog remembers: automatic backtracking is the single control mechanism of the language, and it is what turns one refutation into two answers.

10. Prolog: syntax and execution

Everything above was about logic programming in general. Prolog is a particular language, with particular lexical conventions and one particular search strategy.

Syntax

CategoryConvention
VariablesAlphanumeric strings starting with an uppercase letter or an underscore. The underscore alone is the anonymous variable, a sort of don’t-care variable; an underscore followed by a string is a normal variable during resolution, but does not need to be exposed in the computed substitution
FunctorsAlphanumeric strings starting with a lowercase letter — this holds for both proper functors and constants
TermsBuilt recursively out of functors and variables, as in logic programming: term, Var, f(X), p(Y,f(a)) are Prolog terms; term, var, f(a), p(x,y) are ground terms
PredicatesAlphanumeric strings starting with a lowercase letter — the same as functors
AtomsBuilt by applying predicates to terms: predicate, f(X), p(Y,f(a)) are Prolog atoms

And a remark that opens a door: out of context, parent(lino,joey) could represent either a ground atom or a ground term. Is this an issue or a feature? It paves the way towards meta-programming.

Clauses follow the shape of Horn clauses, with Prolog punctuation:

ClauseFormCondition
clauseA :- B1, ..., Bn.A is the head, B1...Bn the body, :- denotes logic implication, . is the terminator
factA.a clause with no body (n = 0)
ruleA :- B1, ..., Bn.at least one atom in the body (n > 0)
goal:- B1, ..., Bn. often written ?- B1, ..., Bn.no head, at least one atom in the body

A Prolog program is a sequence of Prolog clauses, interpreted as a conjunction of clauses, constituting a logic theory made of Horn clauses written according to Prolog syntax.

Execution

The aim of a Prolog computation: given a program P and the goal ?- p(t1,...,tm) — also called a query — if X1...Xn are the variables in the terms, the meaning of the goal is to query P and find whether there are values for those variables that make the query true. The aim is thus to find a substitution σ = X1/s1, ..., Xn/sn such that P ⊨ p(t1, ..., tm)σ.

As a logic programming language, Prolog adopts SLD resolution. As a search strategy, it applies resolution in a strictly linear fashion: goals are replaced left to right, sequentially; clauses are considered in top-to-bottom order; subgoals are considered immediately once set up. The result is a depth-first search strategy. And in order to achieve completeness, Prolog saves choicepoints for any possible alternative still to be explored, going back to the nearest available choicepoint in case of failure — exploiting automatic backtracking.

The framework used in the lab is SWI-Prolog, available at its home site, on GitHub, and online as SWISH, where you write the program on the left, the goal on the bottom right, and press CTRL-ENTER.

The knowledge base, and two rules

The deck lists seven things to observe while interacting with these programs, and they are a good checklist for anyone new to the paradigm: success; failure; computed substitution; unification; backtracking; clause order; and no input/output parameters — no direction is required for arguments, in principle, thanks to unification.

For the exam

The last item is the one that most surprises programmers coming from other paradigms, and it is the practical face of the declarative/procedural coincidence. sum(X, Y, Z) is not a function from two inputs to one output: it is a relation, and any subset of its arguments may be bound. That is why ?- sum(X, s(s(z)), s(s(s(s(s(s(z))))))) is a legitimate question, and why the same predicate both checks and computes and generates.

11. Numbers, and recursion

In pure logic programming — and in pure Prolog — 3 and + are just symbols with no specific meaning attached. So how do we deal with natural numbers? Giuseppe Peano shows us the way.

The five Peano axioms, introduced in 1889, were meant to provide a rigorous foundation for the natural numbers, and in particular they enable an infinite set to be generated by a finite set of symbols and rules: (1) zero is a natural number; (2) every natural number has a successor in the natural numbers; (3) zero is not the successor of any natural number; (4) if the successor of two natural numbers is the same, then the two original numbers are the same; (5) if a set contains zero and the successor of every number in the set, then the set contains the natural numbers.

To represent them in logic programming we use constants and functors for the numbers, and predicates for relations such as n ∈ ℕ. From the axioms: zero is essential, so we use the constant z; and the notion of successor is essential, so we use the functor s/1. If N is a natural number then s(N) is another natural number — a recursive data structure, where the successor of s(N) is s(s(N)). In our pre-interpretation, z is zero, s(z) is one, s(s(z)) is two. And the predicate nat/1 represents the relation: nat(s(s(z))) means that two is a natural number, according to our current interpretation.

Three remarks close the section, and the first is the one students get wrong: z is zero, 0 is not zero. Recursive data structures are powerful and expressive, yet unreadable for humans and generally impractical — which is why Prolog is actually impure logic programming, offering things like ?- X is 1 + 3. Logic programming allows for the generation of data, for instance the set of natural numbers. There is an unlimited number of SLD refutations here: an infinite branch of the proof tree. And the order of clauses matters in Prolog — what if we exchanged the fact with the rule?

Watch out

Both remarks are traps with the same root: Prolog is a strategy layered on top of a logic. Logically, the order of clauses is irrelevant — a program is a conjunction, and conjunction is commutative. Operationally, Prolog considers clauses top to bottom and depth first, so putting the recursive rule before the base fact turns a terminating program into a non-terminating one without changing its meaning. Likewise 0 is not zero because nothing in the logic attaches arithmetic to symbols: meaning comes from the program, not from the glyph.

12. Lists

Lists are defined via two constructors: nil, the empty list containing no elements; and cons, taking an element H and a list T and generating the list cons(H, T). So cons(a, cons(b, cons(c, nil))) would represent the list a, b, c. They are typical recursive data structures, used to represent sequences of any sort.

In Prolog the two analogous constructors are [] for the empty list — a constant — and . for cons, a functor of arity 2, so that cons(H,T) is .(H,T). Prolog sequence notation simplifies writing lists: .(H,T) can be written [H|T]; .(H,.(H2,T2)) can be written [H,H2|T2]; and there the empty list can be omitted. So [a,b,c] represents the list a, b, c, where a is the head, [b,c] is the tail, and:

mgu([a, b, c], [H|T]) = {H/a, T/[b, c]}

Being recursive data structures, lists are typically handled by recursive rules — which is incidentally also the only way to handle repeated operations over sequences in Prolog, where there is nothing like a cycle programming construct. The recursion scheme follows from the search strategy: since Prolog is depth-first, with clauses used orderly top-down, termination is handled with a fact, typically coming before the recursive rule — exactly as in nat/1 and sum/3.

The typical example: member/2

Checking whether the first argument is a term that is a member of the list in the second argument:

member(X, [X|_]).
member(X, [_|T]) :- member(X, T).

Goals to try, and to discuss:

?- member(b, [a,b,c]).
?- member(b, [a,b,b]).
?- member(X, [a,b,c]).
?- member(blue(X), [red(a), blue(b), red(c), blue(d)]).
?- member(z, X).

The remarks the deck attaches: the search strategy goes left to right through the list; the program devises out all the members of the list; it supports conditional membership, given a certain computed substitution — which is what the blue(X) goal exercises; and it supports the generation of lists, which is what the last goal does, since member(z, X) with X unbound asks for lists containing z rather than testing a given one.

Key idea

Two clauses, five completely different behaviours — test, count, enumerate, pattern-match, generate — and not one line of control flow anywhere. That is what “the logic programmer is concerned with what to compute, while how to compute is delegated to the machinery” means in practice, and it is also why Chapter 7 could describe a BDI intention as a stack of plans handled “similarly to how a Prolog interpreter handles clauses”.

Check your understanding

What is computational logic, in the three-part definition?

Logic in computer science: it concerns all uses of logic in computer science — to compute, to represent computation, and to reason about computation. It is rooted in logic, applied mathematics, artificial intelligence and computer science.

Explain the two notions of truth with the deck’s own examples.

Semantic: “the sum of numbers two and one is three” is true in any formalisation of arithmetic, whatever symbols we use — truth through interpretation. Syntactic: if “the Snark was a Boojum” then “something is a Boojum” is true whatever the meaning of the sentence — truth through symbol manipulation alone.

Define logical entailment and explain why it connects to the syntactic side.

Tarski: when a conclusion is true in every interpretation making all premises true, the premises logically entail the conclusion. If a conclusion is true for any interpretation it is true independently of the interpretation, hence independently of the semantics of the symbols; and if so, it likely depends on the symbols only — therefore the syntactic notion of truth is in principle connected with logical entailment. Example: p and p → q entail q, whatever p and q mean.

Distinguish computation from deduction, after Pfenning.

To compute we start from a given expression and, according to a fixed set of rules (the program), generate a result — 15 + 26 → 41. To deduce we start from a conjecture and, according to a fixed set of rules (axioms and inference rules), try to construct a proof — for instance that a^n + b^n ≠ c^n for n > 2, which took 357 years of hard creative work. Computation is mechanical and requires no ingenuity; deduction is a creative process.

In what two senses do computation and deduction meet?

(1) Computation can be seen as a limited form of deduction, since it establishes theorems: 15 + 26 = 41 is both a computation result and a theorem of arithmetic. (2) Deduction can be considered a form of computation if we fix a strategy for proof search, removing the guesswork and the possibility of ingenuity — that is, if we automate deduction. The latter is the foundation of logic programming: computation proceeds by proof search according to a fixed strategy.

Where does the deck locate the space for autonomy, and what objection does it raise against itself?

Fixing a strategy for proof search removes the space for choice, so autonomous choice and rational deliberation are required whenever deduction is needed and computation cannot be implemented as an automatic technique: whenever computation and deduction do not exactly match, there lies the space for autonomy. The self-objection: autonomous computation is still computation, so in principle it cannot bridge the gap. The resolution: the relation between the two is stated independently of the domain of discourse, so exploiting autonomous choice in non-trivial domains requires domain knowledge, leaning onto the semantic side, which can drive the choices — for instance which path to follow in the proof tree.

What is a proof tree, and what are its leaves?

The data structure covering all possible inference chains for a judgement J: its root is the conclusion J, its edges are deductions labelled by inference rules, and premises are child nodes while conclusions are parent nodes. Its leaves are axioms — indemonstrable first principles that can be used as premises with no need of proof.

Contrast forward and backward chaining, including the direction of traversal.

Forward (forward-reasoning search): start from what we know, initially the axioms; use inference rules to deduce new judgements; add new evidence to old, recursively, until the conjecture is obtained — traversing the tree from the leaves down to the root. Backward (goal-directed search): start from what must be proven, initially the conjecture; use rules to find the judgements it depends on; recursively prove those until only axioms remain — traversing from the root up to the leaves. Resolution leads to backward chaining.

What was missing between Robinson’s resolution and a programming language, and who supplied it?

Robinson’s resolution principle [1965], with unification, allowed proof of first-order theorems and made it possible to compute with logic, but did not provide a proof strategy for a full-fledged language. Kowalski [1974] supplied the procedural interpretation of Horn clauses: by defining logic programs as collections of Horn clauses and restricting Robinson’s principle accordingly, a logical implication became amenable to both a declarative and a procedural interpretation. Prolog followed, by Colmerauer in Marseille, in 1973.

Define terms, atoms and literals.

A term is: a variable; or a functor of arity 0, that is a constant; or f(t1,...,tn) where f is a functor of arity n and the ti are terms. Variables and constants are atomic terms, the rest are structured terms, and the recursive definition makes a term a tree. An atom is p(t1,...,tn) where p is a predicate symbol of arity n; atoms represent elementary propositions. A literal is an atom A or its negation ¬A.

What is a Horn clause, and what is a logic program?

A logic clause is a finite disjunction of literals. A definite clause has exactly one positive literal, A ← B1,...,Bm; a unitary clause is a definite clause with no negative literals, A ←; a definite goal has no positive literal, ← B1,...,Bm. A Horn clause is either a definite clause or a definite goal. A logic program is a CNF of Horn clauses — a conjunction of rules (definite clauses), facts (unitary clauses) and goals.

Describe SLD resolution informally, and its four possible endings.

To prove goal G against program P, look for a clause A ← B1,...,Bn whose head A unifies with G; if mgu(G,A) = θ, the proof of G succeeds if we can further prove B1θ,...,Bnθ, the mgu specialising the clause to the goal at hand; and resolution proceeds recursively. Endings: the current goal becomes empty, giving a successful derivation, an SLD refutation; a selection rule picks the next subgoal when it is not empty; the selected goal matches no clause head, so the proof fails; or the goal never empties while heads keep matching, so the derivation does not terminate.

What are or- and and-non-determinism?

Or-non-determinism: more than one clause could unify through its head with the current goal, and either could be chosen for the resolution step. And-non-determinism: more than one goal could be subject to proof at the same time, and either could be chosen, through a selection rule. Neither choice affects correctness, but how they are exploited determines how the proof tree is explored — and both admit parallel, concurrent or distributed exploitation.

What is Prolog’s search strategy?

SLD resolution applied in a strictly linear fashion: goals replaced left to right, sequentially; clauses considered top to bottom; subgoals considered immediately once set up — hence a depth-first search. To achieve completeness Prolog saves choicepoints for every alternative still to be explored, returning to the nearest one on failure through automatic backtracking, which is the single control mechanism of the language.

How are natural numbers represented in pure logic programming, and what is the classic trap?

Following Peano: the constant z for zero and the functor s/1 for successor, so that s(z) is one and s(s(z)) is two, with the predicate nat/1 for the relation of being a natural number. The base fact nat(z). plus the recursive rule nat(s(N)) :- nat(N). give a finite representation of infinitely many propositions. The trap: z is zero, 0 is not zero — no meaning is attached a priori to symbols. A second trap: clause order matters operationally, so putting the recursive rule before the base fact breaks termination without changing the logical meaning.

Write member/2 and say what its goals demonstrate.

member(X,[X|_]). and member(X,[_|T]) :- member(X,T). The goals demonstrate a search strategy going left to right through the list; the devising of all members of a list; conditional membership given a computed substitution, as in member(blue(X), [red(a), blue(b), red(c), blue(d)]); and the generation of lists, as in member(z, X) with X unbound.