The Logic Paradigm

Imagine describing a family tree not as a sequence of steps, but as a set of facts and relationships. You say "Tom is a parent of Bob," "Bob is a parent of Ann," and "X is a grandparent of Z if X is a parent of Y and Y is a parent of Z." Then you simply ask the question "who is a grandparent of Ann?" You let the machine figure out the answer. No loops, no explicit assignment, no control flow written by hand. This is the essence of logic programming. It is a declarative paradigm in which programs are theories written in a fragment of formal logic. Computation itself is automated deduction. Logic programming matters because it inverts the usual relationship between programmer and machine. In imperative and even functional programming, you still specify how to compute a result. In the logic paradigm, embodied most famously by Prolog, you specify what is true. A general-purpose inference engine then finds values that make your query true. This engine is built on Unification and a search strategy called SLD-resolution. This idea underlies expert systems, rule engines, constraint solvers, static analyzers, and type inference systems. It also powers query languages like Datalog, which drive modern database engines and program-analysis tools. The big picture is that logic programs are collections of Horn clauses. These include facts (unconditional truths) and rules (conditional truths of the form "head if body"). A query is answered by systematically trying to derive it from the facts and rules. Unification binds variables during this process. Backtracking explores alternative derivations when a choice leads to failure. This machinery — resolution plus unification plus backtracking — is a complete decision procedure for a well-understood logical fragment. That is why logic programming has a clean mathematical foundation, shared with Type Systems and automated theorem proving. This article builds the paradigm from the ground up. It covers the vocabulary of facts, rules, and queries. It explains the unification algorithm that makes pattern matching over trees precise. It presents the SLD-resolution procedure that drives query answering. It also covers the depth-first search with backtracking that makes it all executable. We verify the unification algorithm step by step, and trace a real derivation by hand. We are honest about where the paradigm's elegance runs into practical friction — non-termination, the cut operator, and negation as failure.

Key takeaways
  • Logic programs consist of facts and rules (Horn clauses); a query asks what can be derived from them.
  • Unification is the core pattern-matching algorithm: it finds the most general substitution making two terms syntactically equal.
  • SLD-resolution repeatedly replaces a goal with the body of a matching clause, threading substitutions through the derivation.
  • Backtracking (depth-first search over the SLD-tree) recovers from failed choices and enumerates all solutions.
  • Unification runs in near-linear time with the occurs check omitted (as most Prolog implementations do by default).
  • Logic programming's declarative reading ("what is true") coexists with an operational reading ("how resolution proceeds"), and the gap between them causes the paradigm's classic pitfalls.
The core insight

A logic program is a set of logical axioms, and running it means letting unification-driven resolution search for a proof of your query, extracting the answer substitution as the result.

The idea

A logic program is built from terms: constants (`tom`, `42`), variables (`X`, `Y`), and compound terms (`parent(tom, bob)`, `f(X, g(Y))`). A fact is a clause with no body, asserting something unconditionally true, e.g. `parent(tom, bob).` A rule has a head and a body, read as "head holds if body holds," e.g. `grandparent(X, Z) :- parent(X, Y), parent(Y, Z).` Facts and rules are both special cases of Horn clauses: at most one positive literal (the head), plus zero or more negative literals (the body), written in clausal form as `H :- B1, B2, ..., Bn`. A query (or goal) asks whether some statement follows from the program, e.g. `?- grandparent(tom, ann).`, possibly containing variables to be bound in the answer. The two central mechanisms that make this declarative reading executable are unification and resolution. Unification is the algorithm that decides whether two terms can be made syntactically identical by substituting terms for variables, and if so computes the most general unifier (mgu) — the least-committal such substitution. Resolution is the inference rule that, given a goal matching the head of a clause (via unification), replaces the goal with the clause's body, carrying the unifier forward. Repeated resolution steps, restricted to the specific strategy called SLD-resolution (Selective Linear Definite clause resolution), form a derivation; a derivation ending in the empty goal is a proof, and the composed substitutions along the way give the answer. Because a goal can unify with more than one clause head, resolution is nondeterministic: at each step there may be several applicable clauses. Real implementations resolve this with backtracking search, conventionally depth-first and left-to-right (try clauses in program order, subgoals in body order), which turns the search space into an explicit tree — the SLD-tree — that the interpreter explores, retreating on failure to try the next alternative. This combination of unification, resolution, and chronological backtracking is what a Prolog-like system actually executes; understanding it precisely is the payoff of this article.

How it works

Unification. Two terms unify if there is a substitution σ (a finite mapping from variables to terms) such that applying σ to both terms yields syntactically identical results. The classical algorithm (Robinson's algorithm, refined by Martelli–Montanari into a near-linear version) works by maintaining a set of equations to solve and a substitution built so far. ``` function UNIFY(t1, t2):

# returns a substitution (mgu) or FAIL stack = [(t1, t2)] subst = {} # empty substitution while stack is not empty: (a, b) = stack.pop() a = APPLY(subst, a) b = APPLY(subst, b) if a == b: # identical constants/variables/atoms continue else if a is a variable: if a occurs in b: # occurs check (optional; see below) return FAIL subst = COMPOSE(subst, {a -> b}) else if b is a variable: stack.push((b, a)) # swap and retry else if a and b are compound terms: if FUNCTOR(a) != FUNCTOR(b) or ARITY(a) != ARITY(b): return FAIL for i in 1 .. ARITY(a): stack.push((ARG(a, i), ARG(b, i))) else: return FAIL # e.g. two different constants return subst ``` `COMPOSE` applies the new binding to the existing substitution's range and adds the new pair; `APPLY` substitutes a term's variables according to the current substitution. Standard Prolog implementations skip the occurs check by default for efficiency, trading soundness (you can build a term that "contains itself", e.g. unifying `X` with `f(X)`) for speed; this is a documented, deliberate design tradeoff.

SLD-resolution and the interpreter loop. Given a goal list (initially the query's literals) and a program (an ordered list of clauses), the interpreter repeatedly picks the leftmost goal, tries clauses in program order, and on a unifying clause replaces that goal with the clause's body (renamed with fresh variables to avoid name clashes), applying the mgu to the rest of the goal list. ``` function SOLVE(goals, program, subst): if goals is empty: yield subst # success: report this answer return goal = FIRST(goals) rest = REST(goals) for clause in program: # try clauses in program order (head, body) = RENAME_FRESH(clause) # fresh variables per attempt mgu = UNIFY(goal, head) if mgu != FAIL: newGoals = APPEND(body, rest) newSubst = COMPOSE(subst, mgu) yield from SOLVE(newGoals, program, newSubst)

# on backtracking, control returns here and the loop tries

# the NEXT clause -- this is exactly what gives multiple answers ``` `RENAME_FRESH` is essential: each use of a clause must get its own copy of its variables, so that resolving `parent(X, Y)` against two different subgoals does not accidentally conflate unrelated `X`s. The `yield`/backtrack structure is depth-first, left-to-right: it explores the first successful branch fully (and any branches nested inside it) before returning to try the next sibling clause — precisely the standard Prolog search order. The cut operator (`!`), found in real Prolog systems, prunes this search: once execution passes a cut, the interpreter commits to the choices made so far in the current clause and discards remaining alternative clauses for the enclosing goal. It is an operational, non-logical escape hatch layered on top of the pure declarative model, added for efficiency and control — and a classic source of subtle bugs, discussed below.

Figure
The SLD-tree for a query: each node is a goal list, each edge a clause application with its unifier
SLD-tree for ?- grandparent(tom, Z) grandparent(tom, Z) rule: gp(X,Z):-p(X,Y),p(Y,Z) {X/tom} parent(tom,Y), parent(Y,Z) fact: parent(tom,bob) {Y/bob} parent(bob,Z) fact: parent(tom,liz) {Y/liz} parent(liz,Z) fact: parent(bob,ann) {Z/ann} [] success: Z=ann no fact matches fail no fact matches fail Depth-first, left-to-right traversal finds Z=ann via the left branch first; backtracking would continue into the right subtree for further solutions.

Complexity & correctness

Unification complexity. The naive algorithm above, without the occurs check, has worst-case time complexity that depends on term representation. A structure-sharing (DAG) representation, together with the Martelli–Montanari refinement, achieves O(n) where n is the combined size of the two terms — each equation is processed once and substitutions are represented implicitly via union-find-like binding rather than eagerly rewritten. Naive substitution-based implementations that apply bindings eagerly by copying terms can degrade to O(n²) or worse, because a single variable binding can force rewriting of exponentially large terms if done carelessly. The occurs check, which guarantees soundness by rejecting circular bindings like `X = f(X)`, adds an extra traversal per variable binding; in the worst case this makes full-generality unification O(n²) (or with sophisticated shared structures, still roughly linear, but real Prolog systems mostly just omit it, trading soundness for the common case's speed).

Why omitting the occurs check is dangerous but usually harmless. Without the check, unifying `X` with `f(X)` succeeds and produces a rational tree (an infinite, cyclic term), which most operations never need to traverse fully; it becomes a correctness hazard only when a program explicitly ever tries to print, compare structurally, or otherwise traverse such a term to a fixpoint. Most useful logic programs never generate self-referential unifications, so in practice this optimization is safe; it is a documented unsoundness in the standard semantics, not a bug.

Correctness of SLD-resolution. The precise correctness statement is a soundness and completeness theorem relating the operational semantics (the SOLVE algorithm) to the declarative semantics (logical entailment in the least Herbrand model of the program, viewed as a first-order theory of Horn clauses).

  • Soundness: if `SOLVE` succeeds on goal G with substitution σ, then Gσ is a logical consequence of the program P, i.e. P ⊨ Gσ. This holds because every resolution step is a valid instance of the resolution inference rule from first-order logic, which is itself sound.
  • Completeness: if P ⊨ Gσ for some ground instance, then there exists an SLD-derivation (some sequence of clause choices) that succeeds and produces a substitution at least as general as σ. This is the SLD-resolution completeness theorem (a consequence of the general completeness of resolution for Horn-clause logic).

The crucial subtlety is that completeness is a statement about the existence of a successful branch in the SLD-tree, not about the specific depth-first, left-to-right search strategy finding it in finite time. Depth-first search can dive into an infinite branch (e.g., a recursive rule with no terminating case reachable) and never backtrack to a finite successful branch that lies to its right. So: SLD-resolution as a proof system is sound and complete; Prolog's specific search strategy is sound but not complete — it can loop forever even when a proof exists elsewhere in the tree. This is exactly analogous to how a sound and complete type system can still have an undecidable or non-terminating inference algorithm if implemented with the wrong search order, a tension familiar from Evaluation and Reduction of Expressions where reduction strategy (call-by-value vs. normal order) affects termination even when a normal form exists.

Complexity of query answering is therefore not a single number: it depends on the size of the SLD-tree actually explored, which depends on clause ordering, goal ordering, and backtracking depth — in the worst case exponential in the number of subgoals, since each may spawn multiple clause choices whose combinations multiply. This mirrors backtracking search generally: the branching factor is bounded by the number of clauses matching a predicate, and the depth is bounded by the recursion depth of the derivation, giving a worst-case bound of O(bᵈ) tree nodes for branching factor b and depth d, with the usual caveats that cuts and indexing can prune this dramatically in practice.

Worked example

Consider this small program: ```prolog parent(tom, bob). parent(tom, liz). parent(bob, ann). parent(bob, pat). grandparent(X, Z) :- parent(X, Y), parent(Y, Z). ?- grandparent(tom, Z). ``` We trace `SOLVE([grandparent(tom, Z)], program, {})` by hand, showing each step's goal list, the clause tried, the resulting unifier, and the outcome.

StepGoal listClause triedUnificationResult
1`grandparent(tom, Z)``grandparent(X,Z') :- parent(X,Y), parent(Y,Z')` (renamed)`X/tom, Z'/Z`expand to body
2`parent(tom,Y), parent(Y,Z)``parent(tom, bob).``Y/bob`goal 1 solved
3`parent(bob, Z)``parent(bob, ann).``Z/ann`success, answer `Z = ann`
4 (backtrack into step 3)`parent(bob, Z)``parent(bob, pat).``Z/pat`success, answer `Z = pat`
5 (backtrack into step 2)`parent(tom,Y), parent(Y,Z)``parent(tom, liz).``Y/liz`goal 1 re-solved
6`parent(liz, Z)`no matching fact—fail, dead end
7 (backtrack fully)`grandparent(tom,Z)`no more clauses—search exhausted

Reading the trace: the interpreter first commits to `Y = bob` (Tom's first-listed child), finds `ann` as a grandchild, reports it, and — if the caller asks for more solutions (`;` in a Prolog REPL) — backtracks to find `pat` via the same `Y = bob` branch, then backtracks further to try `Y = liz`, which yields no grandchildren since `liz` has no listed children, and the whole search terminates having found exactly `{Z = ann, Z = pat}`. This demonstrates unification producing different bindings on each branch (not a "trivial symmetric" single-answer case), and shows backtracking recovering from the dead-end at `liz` rather than crashing or looping. Now trace unification itself on the first clause application, to see the algorithm's internal state:

Stack (top first)Substitution built
`(grandparent(tom,Z), grandparent(X,Z'))``{}`
`(tom, X), (Z, Z')` — after decomposing functor/arity match`{}`
`(Z, Z')``{X/tom}` (bound `X`, since `tom` is not a variable but `X` is — algorithm swaps and binds)
`{}` (stack empty)`{X/tom, Z'/Z}`

This matches step 1's claimed unifier exactly, confirming the hand-trace is consistent with the formal `UNIFY` procedure.

Common pitfalls & traps

PitfallWhy it happensConsequence
Infinite recursion via left-to-right, depth-first searchA recursive rule's base case is not tried before its recursive case in program orderQuery never terminates even though a finite proof exists elsewhere in the SLD-tree
Misplaced or overused cut (`!`)Cut commits to choices made so far in the current clause, discarding alternative clauses and pending backtrack points; placing it too early prunes valid solutionsCorrect-looking programs silently lose answers on backtracking
Negation as failure (`\+ G`) treated as classical negation`\+ G` succeeds only if G finitely fails, not if G is logically false; with unbound variables in G this gives unsound or unintuitive results (the "floundering" problem)Programs give wrong answers for queries with free variables inside negated goals
Omitted occurs check causing cyclic termsReal Prolog systems skip the occurs check by default for speedUnifying `X` with `f(X)` silently succeeds, and later structural traversal of that term may loop forever
Variable capture across clause usesForgetting to rename clause variables freshly on each use (implementation bug, not a user error, but a classic bug when hand-building interpreters)Two unrelated subgoals accidentally share bindings, corrupting the derivation

Pioneers & history

The logic paradigm grew out of automated theorem proving research seeking to make resolution — a general inference rule for first-order logic discovered in the early 1960s — practical as a programming mechanism rather than just a proof-search technique.

Origins · 1 minRobert Kowalski and how this was discovered
RK
Robert Kowalski
born 1941 · Formalized the "Logic + Control = Algorithm" view and co-developed SLD-resolution as the procedural interpretation of Horn clause logic

AC
Alain Colmerauer
1941-2017 · Led the team that built Prolog at Marseille, the first practical programming language based on logic and unification

History
1965
— J. A. Robinson publishes the resolution principle, giving a single, mechanizable inference rule for first-order logic.
1972
— Colmerauer's team in Marseille implements the first Prolog interpreter, built around unification and backtracking.
1974
— Kowalski articulates the procedural interpretation of Horn clauses, crystallizing "Algorithm = Logic + Control."
1983
— David H. D. Warren's Warren Abstract Machine (WAM) gives Prolog an efficient compiled execution model.
1987
— Datalog, a restricted, always-terminating logic-programming fragment, gains traction for deductive databases and later for program analysis.

Warren's abstract machine deserves particular note: it showed that the elegant but seemingly slow unification-and-backtracking model could be compiled to an efficient low-level instruction set, much as the Evaluation and Reduction of Expressions machinery for functional languages was later compiled to abstract machines like the SECD or STG machine — a recurring theme that declarative elegance and efficient execution are not in tension once the right intermediate representation is found.

Where this matters

Prolog and its descendants remain the direct ancestors of modern constraint logic programming systems used in scheduling, planning, and combinatorial optimization, where facts and rules naturally express constraints and the solver's search is the constraint propagation engine. Datalog, a terminating, function-symbol-free fragment of the same paradigm, is the logical core of modern deductive database query optimizers and, notably, of large-scale static analysis and security tools that express points-to analysis, taint tracking, and access-control policies as sets of Datalog rules evaluated over enormous fact databases. Rule-based expert systems and business-rule engines used in finance, insurance, and healthcare compliance are direct descendants of the fact-and-rule model. Prolog-style unification is also embedded, in disguise, inside every language with algebraic pattern matching and inside type inference algorithms — Hindley–Milner type inference is itself, at its core, a unification algorithm over type terms, tying this paradigm directly back to Type Systems. Even SQL query planners and modern build systems that resolve dependency graphs by declarative rule satisfaction owe a conceptual debt to SLD-resolution's model of goal-directed proof search.

Ask the tutor: How does Prolog's Warren Abstract Machine compile unification and backtracking into efficient low-level instructions?

What exactly is a Horn clause, and why restrict logic programs to them?

A Horn clause is a disjunction of literals with at most one positive literal, written procedurally as `H :- B1, ..., Bn` (a rule) or just `H` (a fact, n = 0) or as a headless clause used for goals. Restricting to Horn clauses matters because this fragment of first-order logic has a uniquely simplest resolution strategy, SLD-resolution, that is both sound and complete for it, and admits an efficient operational reading: "to prove H, prove all the Bi." General first-order logic supports resolution too, but without the Horn restriction there can be multiple positive literals in a clause, breaking the simple "head implies body" procedural reading and requiring much more complex proof search. Horn clauses are exactly expressive enough for a huge class of practical declarative programs while staying computationally tractable.

How is unification different from ordinary pattern matching, as seen in functional languages?

Ordinary pattern matching (as in a `match` or `case` expression) is one-directional: a concrete value is matched against a pattern containing variables, and only the pattern side may bind variables. Unification is two-directional: both sides may contain variables, and both sides may get bound. For example, unifying `f(X, b)` with `f(a, Y)` binds `X` to `a` and `Y` to `b` simultaneously, something one-directional pattern matching cannot express since neither term is a fully concrete value. This symmetry is exactly what lets logic programs run "backwards" — the same clause can compute an output from an input or search for an input given an output, a flexibility ordinary pattern matching does not provide.

Why does clause and goal ordering affect whether a Prolog program terminates, if the underlying logic doesn't care about order?

The declarative semantics (what is logically true given the program) is indeed order-independent: a program's least Herbrand model doesn't change if you reorder facts, rules, or the literals in a rule body. But the operational semantics — what the depth-first, left-to-right interpreter actually does — is highly order-sensitive, because it commits to trying clauses and subgoals in a fixed sequence without look-ahead. Putting a recursive clause before its base case, or putting a non-terminating subgoal before a terminating one in a conjunction, can send the search down an infinite branch that a different order would have avoided. This is the fundamental gap between "a proof exists" (declarative, order-independent) and "the interpreter finds it in finite time" (operational, order-dependent).

What problem does the cut operator solve, and what does it cost?

Cut (`!`) lets the programmer tell the interpreter "stop exploring alternatives for choices made earlier in this clause," which prunes the SLD-tree and both speeds up execution and prevents redundant or nonsensical extra solutions, e.g., committing to the first matching case in an if-then-else-style rule. The cost is that cut is not part of the logical reading of the program at all — it is a purely operational, procedural annotation bolted onto a declarative language. A clause with a cut can no longer be freely reordered, reused backward, or reasoned about purely via logical entailment, because cut's effect depends on the specific left-to-right, depth-first execution order. This tension between declarative purity and pragmatic control is one of the paradigm's most debated design points.

Is logic programming still just theoretical, or is it used in production systems today?

It is very much in production use, though often invisibly. Datalog and Datalog-like engines underlie modern static analysis and security-vulnerability tools that scale to millions of lines of code, expressing analyses like points-to or taint propagation as declarative rules evaluated efficiently via bottom-up fixpoint computation rather than Prolog-style top-down search. Constraint logic programming descendants power industrial scheduling and planning software. Business rule engines used in insurance underwriting, fraud detection, and regulatory compliance are direct architectural descendants of fact-and-rule systems. Even outside dedicated logic languages, unification appears inside type inference engines (Hindley–Milner) and query planners, so the paradigm's core mechanism is embedded throughout modern software infrastructure.

How does SLD-resolution relate to general resolution used in automated theorem proving?

General resolution is a sound and complete inference rule for arbitrary clauses in first-order logic (not just Horn clauses), and full theorem provers use it with various clause-selection heuristics to search a much larger space. SLD-resolution is a highly restricted, linear strategy specialized to Horn clauses: at each step there is exactly one "current goal list" being refined (not an arbitrary growing set of derived clauses), and clause selection always resolves against a clause from the original program, never against a previously derived intermediate clause. This restriction is what makes SLD-resolution's search space small and structured enough to serve as an actual programming language's execution model, at the cost of only working for the Horn-clause fragment rather than full first-order logic.

Before this