Logic programming: Prolog, unification and resolution
From propositional logic to Prolog: fact, rule and query, unification and instantiation, depth-first search with resolution and backtracking, lists, arithmetic (is), cut and the closed-world assumption. Finding the answers to queries and their order.
Logic programming writes a program with logical propositions: you say what is true, and the system tries to prove the asked question from these propositions. That is why it is a declarative approach: you describe what the result is, not how to compute it. The language of this course is Prolog (1972, Colmerauer and Roussel). What comes up on exams: a database and rules are given, and you are asked to write all the answers to a query in order and to find the results of unifications.
Golden rules:
- Clauses are tried from top to bottom, subgoals from left to right.
- On failure, go back to the most recent choice point (backtracking).
- What starts with a capital letter is a variable, what starts with a lowercase letter is an atom.
iscomputes,=only unifies.- Cut prevents other clauses from being tried.
From logic to Prolog
A proposition is a logical statement that can be true or false. Predicate calculus writes propositions with compound terms: parent(ayse, can), a functor (parent) and arguments. For propositions with variables there are quantifiers: (for every X), (there exists an X).
For automatic proof, propositions are converted to a standard form: clausal form. Prolog uses a restricted version of this too: the Horn clause. A Horn clause has at most one term on its left side (the conclusion part):
Reading: “If parent(X, Y) and parent(Y, Z) are true, then grandparent(X, Z) is true.” Horn clauses with a non-empty left side are Prolog’s facts and rules.
Prolog’s three constructs
% fact: unconditionally true
parent(ayse, can).
parent(ayse, deniz).
parent(mehmet, can).
parent(can, elif).
parent(deniz, kerem).
parent(deniz, lale).
male(mehmet). male(can). male(kerem).
% rule: head :- body. comma = AND
father(X, Y) :- parent(X, Y), male(X).
grandparent(X, Z) :- parent(X, Y), parent(Y, Z).
?- parent(X, can). % query (goal)
X = ayse ;
X = mehmet.
| Term | Example | Note |
|---|---|---|
| Atom | ayse, can, [] | Starts with a lowercase letter or is quoted |
| Number | 17, -3 | |
| Variable | X, Kim, _ | Capital letter or underscore. _ is the anonymous variable |
| Structure | parent(ayse, can), f(X, g(a)) | functor(arguments) |
A variable “taking a value” is not an assignment; instantiation is temporary during a search for a proof and is undone on backtracking. Variables with the same name inside a clause refer to the same thing; those in different clauses are independent.
Unification
The process of finding the variable bindings that make two terms the same. Rules:
- An atom or a number matches only itself.
- An unbound variable matches any term and is bound to it.
- Two structures match if their functors and number of arguments are the same and their arguments match in order.
?- f(X, b) = f(a, Y). X = a, Y = b
?- f(X, X) = f(a, b). false (X cannot be both a and b)
?- g(X, h(Y)) = g(k, h(X)). X = k, Y = k
?- [H|T] = [1, 2, 3]. H = 1, T = [2,3]
?- [A, B|C] = [x, y, z, w]. A = x, B = y, C = [z,w]
Resolution and search order
Resolution means proving a goal by finding a clause that unifies with that goal and taking the clause’s body as new goals. When the goal list is empty, the proof is complete. Prolog does backward chaining: it starts at the goal and goes backward toward the facts (starting from the facts and deriving new facts is forward chaining; if the goals are few and the facts are many, backward is more efficient).
Prolog’s decisions:
- Depth-first: it does not move on to the next subgoal before finishing the proof of one. Cheap in terms of memory.
- Clause order: in the order written in the database, from top to bottom.
- Subgoal order: from left to right.
- Backtracking: when a subgoal fails, it returns to the subgoal where the last choice was made and tries its next alternative.
Trace: ?- grandparent(ayse, G).
grandparent(ayse, G) :- parent(ayse, Y), parent(Y, G).
parent(ayse, Y) 1st solution Y = can
parent(can, G) G = elif → ANSWER 1: G = elif
no other parent(can, _) → backtrack
parent(ayse, Y) 2nd solution Y = deniz
parent(deniz, G) G = kerem → ANSWER 2
G = lale → ANSWER 3
parent(ayse, Y) no other solution → done
?- grandparent(ayse, G).
G = elif ; G = kerem ; G = lale.
?- father(F, can).
F = mehmet.
For father(F, can): first parent(F, can) → F = ayse; male(ayse) fails → backtrack; F = mehmet, male(mehmet) ✔.
The sibling rule and \=
sibling(X, Y) :- parent(P, X), parent(P, Y), X \= Y.
?- sibling(can, S).
S = deniz.
?- sibling(kerem, S).
S = lale.
Without \= everyone would also come out as their own sibling.
Arithmetic
?- X is 2 + 3 * 4. X = 14
?- X = 2 + 3. X = 2+3 (a structure term, not computed)
?- X = 2 + 3, Y is X * 2. X = 2+3, Y = 10
?- X is Y + 1. instantiation error
?- 6 =:= 2 * 3. true (=:= computes both sides and compares)
is evaluates the right side and unifies it with the left side. All variables on the right must be bound beforehand. That is why the order of subgoals matters in rules containing arithmetic:
len([], 0).
len([_|T], N) :- len(T, N1), N is N1 + 1.
fact(0, 1).
fact(N, F) :- N > 0, N1 is N - 1, fact(N1, F1), F is N * F1.
speed(ford, 100). time(ford, 2).
distance(C, D) :- speed(C, S), time(C, T), D is S * T.
?- len([a, b, c], N). N = 3
?- fact(4, F). F = 24
?- distance(ford, D). D = 200
Lists
The notation [a, b, c] and [H|T]. Classic relations:
member(X, [X|_]).
member(X, [_|T]) :- member(X, T).
append([], L, L).
append([H|T], L, [H|R]) :- append(T, L, R).
?- member(X, [p, q]). X = p ; X = q
?- append([1], [2, 3], L). L = [1,2,3]
?- append(X, Y, [1, 2]). X = [], Y = [1,2] ;
X = [1], Y = [2] ;
X = [1,2], Y = []
Predicates are relations, not functions: the same append joins, splits, looks for a prefix or a suffix. The query determines which argument is input and which is output.
Cut and negation
The cut (!) is a goal that always succeeds; but backtracking cannot pass back through it. The moment it is passed, the choices made for this predicate (both the earlier subgoals and the remaining clauses) are frozen.
max(X, Y, X) :- X >= Y, !.
max(_, Y, Y).
maxbad(X, Y, X) :- X >= Y.
maxbad(_, Y, Y).
?- max(7, 3, M). M = 7.
?- maxbad(7, 3, M). M = 7 ; M = 3. (the second answer is wrong)
The cut gains efficiency but ties the meaning of the program to the clause order; it moves away from pure logic.
Negation as failure: \+ G succeeds if G cannot be proved. This relies on Prolog’s closed-world assumption: everything that cannot be proved from the database is false.
?- \+ parent(elif, _). true (elif's child is not recorded)
?- \+ parent(can, _). false
It is not the same as logical “not”: \+ does not bind a variable and there is no “unknown” state.
Prolog’s shortcomings
| Problem | Explanation |
|---|---|
| Control of solution order | Two logically identical programs can behave differently; the programmer has to think about the order |
| Left recursion | p :- p, ... infinite loop |
| Closed-world assumption | “I don’t know” and “false” cannot be separated |
| Negation | \+ is not logical not |
| Efficiency | Large searches are slow without cut |
bad_anc(X, Y) :- bad_anc(Z, Y), parent(X, Z). % left recursion, first
bad_anc(X, Y) :- parent(X, Y).
ancestor(X, Y) :- parent(X, Y). % base first
ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
?- bad_anc(ayse, elif). infinite recursion (the stack overflows)
?- ancestor(ayse, D). D = can ; D = deniz ; D = elif ; D = kerem ; D = lale
Application areas: relational database queries, expert systems, natural language processing, proof systems.
Common mistakes
| Wrong | Correct |
|---|---|
Can is an atom | Capital letter: a variable. The atom is can |
| Writing the answers alphabetically | Database order and depth-first search |
X = 2 + 3 → X = 5 | X = 2+3; use is to compute |
X is X + 1 increments X | Never true; variables are not reassigned |
N is N1 + 1 before the recursion | N1 is not yet bound: error |
f(X, X) = f(a, b) → X = a | false |
| Cut only cuts the following clauses | Also the alternatives of earlier subgoals in the body |
\+ G = logical not | Unprovability (closed world) |
Test yourself
Exam-style questions. Solve them on paper first, then reveal the answer.
Given the facts below, what are all answers to ?- parent(X, can). in order?
parent(ayse, can).
parent(ayse, deniz).
parent(mehmet, can).
parent(can, elif).
parent(deniz, kerem).
parent(deniz, lale).Show answer
Prolog scans the database from top to bottom and tries to unify the query with each fact. 1st fact: X = ayse ✔. When the user asks for another answer with ;, it continues the search by backtracking: the 2nd fact has deniz, not can, so it doesn't fit; the 3rd fact gives X = mehmet ✔. The answers come in database order.
With the same facts and the rule below, what are all answers to ?- grandparent(ayse, G).?
grandparent(X, Z) :- parent(X, Y), parent(Y, Z).Show answer
The rule: X is a grandparent of Z if X has a child Y and Y has a child Z. The first subgoal parent(ayse, Y): Y = can → parent(can, Z): Z = elif. Backtrack: can has no other child, Y = deniz → Z = kerem, then Z = lale. Order: first all the continuations with the first solution of the first subgoal, then the second solution.
With the same facts, what are all answers to ?- sibling(can, S).?
sibling(X, Y) :- parent(P, X), parent(P, Y), X \= Y.Show answer
can's parents are ayse and mehmet. P = ayse: ayse's children are can (can \= can fails) and deniz ✔. P = mehmet: his only child is can, eliminated. \= means "cannot be unified"; without it everyone would be their own sibling.
What does each unification produce? (a) f(X, b) = f(a, Y) (b) f(X, X) = f(a, b) (c) g(X, h(Y)) = g(k, h(X))
Show answer
Unification finds the variable bindings that make two terms the same. (a) Argument by argument: X ↔ a, b ↔ Y. (b) The first argument makes X = a; in the second, X (now a) would have to match b: it fails. (c) X = k; then h(Y) = h(X) → Y = X = k.
What are H and T after ?- [H|T] = [1, 2, 3].? And A, B, C after ?- [A, B|C] = [x, y, z, w].?
Show answer
[H|T] splits the list into a head (the first element) and a tail (the rest of the list); like Scheme's car and cdr. The tail is always a list. [A, B|C] separates the first two elements and gives the rest to C.
With the standard append/3 below, how many solutions does ?- append(X, Y, [1, 2]). have? List them in order.
append([], L, L).
append([H|T], L, [H|R]) :- append(T, L, R).Show answer
In Prolog predicates are relations, there is no direction: besides joining, append also splits. The first clause gives X = []. The second clause takes the head element into X and recurses with the remaining list. A list with n elements has n + 1 splits.
What does each query print? (a) ?- X is 2 + 3 * 4. (b) ?- X = 2 + 3. (c) ?- X is Y + 1. (Y unbound)
Show answer
is evaluates the right side arithmetically, with the usual precedence: 2 + 12 = 14. = does not compute, it only unifies: X becomes the structure term +(2, 3). Every variable on the right of is must be bound beforehand, otherwise an error. Also X is X + 1 is never true: it does not change X, and if bound it compares it with itself.
What does ?- len([a, b, c], N). return?
len([], 0).
len([_|T], N) :- len(T, N1), N is N1 + 1.Show answer
The length of the empty list is 0. Otherwise if the length of the tail is N1, then N = N1 + 1. Recursion: len([b,c]) → len([c]) → len([]) = 0, then on the way back 1, 2, 3. _ is the anonymous variable: its value doesn't matter. N is N1 + 1 must come after the recursion, because only then is N1 bound.
What does ?- fact(4, F). return?
fact(0, 1).
fact(N, F) :- N > 0, N1 is N - 1, fact(N1, F1), F is N * F1.Show answer
fact(4) = 4 · fact(3) = 4 · 3 · fact(2) = ... = 4 · 3 · 2 · 1 · 1 = 24. The N > 0 condition is important: without it, for fact(0, F) after the first answer backtracking would enter the second rule and descend forever into negative numbers.
What are all answers to ?- max(7, 3, M). and to ?- maxbad(7, 3, M).?
max(X, Y, X) :- X >= Y, !.
max(_, Y, Y).
maxbad(X, Y, X) :- X >= Y.
maxbad(_, Y, Y).Show answer
When the cut (!) is passed successfully, it freezes the choices made so far for this predicate: no other clause is tried. In max the first clause gives 7 and the cut closes the second clause. In maxbad backtracking enters the second clause too and produces a wrong answer (3). The cut gains efficiency but makes the program order-dependent and hard to read.
Why does ?- bad_anc(ayse, elif). never terminate, while the ancestor version answers correctly?
bad_anc(X, Y) :- bad_anc(Z, Y), parent(X, Z).
bad_anc(X, Y) :- parent(X, Y).
ancestor(X, Y) :- parent(X, Y).
ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).Show answer
Prolog tries clauses from top to bottom, subgoals from left to right, depth-first. The first subgoal of bad_anc's first clause is again bad_anc: infinite recursion without looking at any fact (the stack overflows). Logically the two definitions are the same, but Prolog's solution order forces the programmer to think about the order of clauses and goals. One of the biggest flaws of pure logic programming.
Given only the facts above, what do ?- \+ parent(elif, _). and ?- \+ parent(can, _). return, and what assumption does this rely on?
Show answer
\+ G succeeds if G cannot be proved. elif has no child in the database → true. can has one (elif) → false. This is not a logical not: it is the assumption "if it can't be proved, it is false" (closed-world assumption). Everything not in the database is taken to be false; Prolog cannot say "I don't know".
Verification: Since Prolog is not installed, a small Prolog interpreter was written in node (verify/SEN2001/lib/prolog.mjs: unification, depth-first SLD resolution and backtracking, cut, is, comparisons, \=, \+, lists). Every query on the page was run in this interpreter and the answers were listed in the order real Prolog prints them (verify/SEN2001/14.mjs). That the left-recursive rule falls into an infinite loop was also observed in the interpreter (stack overflow); that the fact with the N > 0 condition removed descends forever at the second answer was tested in verify/SEN2001/01.mjs.