1.1 The logic programming idea
A logic program does not describe how to compute. It states facts and rules about a world, and computation happens when you ask a query — the system searches for every way to make the query true.
Algorithm = Logic + Control (Robert Kowalski). The programmer supplies the logic; the language supplies the control (the search strategy).
1.2 The vocabulary of logic programs
| Concept | Syntax example | Meaning |
|---|---|---|
| Constant (atom) | ram, apple, x1 | a specific individual (lowercase) |
| Variable | X, Who, _ | stands for an unknown (Uppercase) |
| Functor / structure | date(12, jun, 2026) | a compound term |
| Predicate | parent(ram, lav) | a relation between terms |
| Fact | parent(ram, lav). | unconditionally true clause |
| Rule | Head :- Body. | "Head is true if Body is true" |
| Query / goal | ?- parent(ram, X). | "find X making this true" |
Facts + rules about the same predicate form a procedure; the whole collection is the database (or program).
1.3 A first program — the family database
% ----- facts -----
parent(dasharath, ram).
parent(dasharath, lakshman).
parent(ram, lav).
parent(ram, kush).
male(dasharath). male(ram). male(lakshman). male(lav). male(kush).
% ----- rules -----
father(X, Y) :- parent(X, Y), male(X).
grandparent(X, Z) :- parent(X, Y), parent(Y, Z).
sibling(X, Y) :- parent(P, X), parent(P, Y), X \= Y.
Read :- as "if" and the comma as "and". Queries:
?- father(ram, lav). % yes/true
?- grandparent(dasharath, G).
G = lav ;
G = kush.
?- sibling(lav, X).
X = kush.
One definition answers questions in every direction — parent(X, lav) finds the parent, parent(ram, Y) finds the children, parent(X, Y) enumerates every pair. Relations, unlike functions, have no fixed inputs and outputs. This bidirectionality is logic programming's superpower.
1.4 Horn clauses — the underlying logic
Every clause is a Horn clause: a disjunction of literals with at most one positive literal.
| Clause form | Logic reading | Program role |
|---|---|---|
A. | A is true | fact |
A :- B1, ..., Bn. | B1 ∧ ... ∧ Bn → A | rule |
?- B1, ..., Bn. | is B1 ∧ ... ∧ Bn satisfiable? | query |
Variables in facts/rules are read universally ("for all X..."); variables in a query are read existentially ("does there exist X...").
1.5 Unification — the engine of matching
Unification makes two terms identical by finding a substitution for their variables. It generalises both pattern matching and equality testing.
| Attempted unification | Result |
|---|---|
parent(X, lav) with parent(ram, lav) | succeeds, X = ram |
date(D, jun, Y) with date(12, M, 2026) | succeeds, D=12, M=jun, Y=2026 |
f(X, X) with f(a, b) | fails (X cannot be both a and b) |
X with g(Y) | succeeds, X = g(Y) — variables can bind to structures |
Rules:
- A constant unifies only with itself or a variable.
- A variable unifies with anything (and the binding then applies everywhere).
- Structures unify if the functor and arity match and the arguments unify pairwise.
The most general such substitution is the MGU (most general unifier) — e.g. for f(X) and f(Y), the MGU is X = Y, not X = a, Y = a.
1.6 How a query actually runs (first look)
To solve ?- grandparent(dasharath, G).:
- Find a clause whose head unifies with the goal → the
grandparentrule, binding X = dasharath, Z = G. - Replace the goal with the rule body:
parent(dasharath, Y), parent(Y, G). - Solve left to right:
parent(dasharath, Y)matches the first fact → Y = ram. - Now solve
parent(ram, G)→ G = lav. Answer: G = lav. - Ask for more (
;) → the system backtracks, retries step 4 → G = kush; then retries step 3 → Y = lakshman, butparent(lakshman, G)has no match → no more answers.
This goal-replacement process is SLD resolution, and the systematic retrying is backtracking — both are studied formally in the computation-model lesson.
1.7 The unification algorithm, step by step
To unify terms T1 and T2 (exam-reproducible form):
- Both constants: succeed iff they are the same constant.
- One is a variable X: bind
X := other term(after the occurs check, below) and apply the binding to all remaining goals. - Both compound: succeed iff same functor and same arity, then unify arguments pairwise left to right, threading the bindings through.
- Otherwise: fail.
Definition (occurs check). Before bindingX := T, verify thatXdoes not occur insideT. UnifyingXwithf(X)would otherwise build the infinite termf(f(f(...))). Standard Prolog omits this check for speed — a known soundness compromise;unify_with_occurs_check/2performs it properly.
Worked unification with working shown:
Unify p(X, g(X), Y) with p(a, Z, h(Z))
1. X vs a → X = a (rule 2)
2. g(X) vs Z i.e. g(a) vs Z → Z = g(a)
3. Y vs h(Z) i.e. Y vs h(g(a)) → Y = h(g(a))
MGU: { X = a, Z = g(a), Y = h(g(a)) } ✓
More practice rows (cover the answers and try them):
| Pair | Result | |
|---|---|---|
f(X, b) and f(a, Y) | X = a, Y = b | |
f(X, X) and f(Y, g(Y)) | fails — occurs check (X = Y and Y = g(Y)) | |
| `[H\ | T] and [1, 2, 3]` | H = 1, T = [2, 3] |
[X, Y] and `[1\ | Z]` | X = 1, Z = [Y] |
p(X) and q(X) | fails — different functors |
1.8 Declarative and procedural readings — one rule, two meanings
Every rule can (and in exams should) be read both ways:
grandparent(X, Z) :- parent(X, Y), parent(Y, Z).
- Declarative reading: "for all X, Y, Z: X is a grandparent of Z if X is a parent of Y and Y is a parent of Z."
- Procedural reading: "to solve
grandparent(X, Z): first solveparent(X, Y), then solveparent(Y, Z)."
The declarative reading is order-independent logic; the procedural reading is what the machine does, where goal order and clause order matter. Kowalski's equation Algorithm = Logic + Control names exactly this split.
1.9 A query trace as a search tree
?- sibling(lav, S). against the family database:
Note how the first candidate S = lav is generated and then rejected by the inequality — generate-and-test in miniature, and a preview of how backtracking quietly powers everything.
1.10 Functional vs logic programming — the bridge table
| Functional (Haskell) | Logic (Prolog) | |
|---|---|---|
| Core unit | function: inputs → one output | relation: no fixed direction |
| Invocation | expression evaluation | query + proof search |
| Matching | one-way pattern matching | two-way unification |
| Multiple results | returned in a list | enumerated by backtracking |
| "No answer" | Nothing / empty list | the query fails |
| Determinism | always exactly one value | zero, one or many solutions |
| Control | reduction order (lazy) | clause order + goal order + cut |
Exam pointers
- "Define fact, rule, query, Horn clause" — one line each, with the
A :- B1,...,Bn↔B1 ∧ ... ∧ Bn → Atranslation for Horn clauses. - "Explain unification with an example" — the 4-rule algorithm, the worked trace of §1.7, and the occurs-check definition earn full marks.
- Family-tree rule-writing (uncle, cousin, ancestor...) is the most common programming question — always add
X \= Yto sibling-like rules to exclude self-pairs. - State both readings (declarative + procedural) whenever asked to "explain how this rule works".
Self-check
- Which of these unify, and with what MGU?
date(D, M, 2026)vsdate(1, jan, Y);f(g(X))vsf(Y, Z);Xvsf(X). - Write rules for
mother/2,cousin/2anddescendant/2givenparent/2,male/1,female/1. - Why are variables in facts read universally but in queries existentially?
- Give the MGU of
f(X, Y)andf(Y, a)— careful, it has two bindings. - What is the difference between matching
f(X, X)againstf(a, a)and againstf(a, b)?