Resolution, Unification, and Backtracking

Author
Affiliation

Clayton Cafiero

University of Vermont

Published

2026-05-25

Resolution and unification

Let’s look more carefully at how Prolog goes about finding satisfaction to a query.

There’s a famous syllogism from introductory logic:

All humans are mortal.
Socrates is a human.
Therefore, Socrates is mortal.

This is an instance of modus ponens: given P \implies Q and P, we may conclude Q.

\dfrac{P \Rightarrow Q \qquad P}{Q}

The antecedents appear above the line; the consequent below.

\dfrac{\text{Socrates is human.} \Rightarrow \text{Socrates is mortal.} \qquad \text{Socrates is human.}}{\text{Socrates is mortal.}}

Prolog’s inference mechanism works on exactly this principle.

Resolution

Consider this knowledge base:

human(socrates).

mortal(X) :-
    human(X).

and the query

?- mortal(socrates).

Prolog works backward from the goal mortal(socrates). It searches the knowledge base for a clause whose head can be unified with mortal(socrates). The rule mortal(X) :- human(X) matches, with X bound to socrates. The original goal is now replaced by the body of the matching rule: prove human(socrates).

human(socrates) is a fact—a bare assertion with no conditions—so it matches directly. The derivation is complete.

We can trace this in sequent notation, where \vdash G means “G can be derived”:

\dfrac{\vdash human(socrates)}{\vdash mortal(socrates)}

Read from bottom to top: to prove mortal(socrates), it suffices to prove human(socrates). Since human(socrates) is a fact (an axiom), it holds unconditionally, and the original goal is established.

true.

This backward-chaining process—finding a rule whose head unifies with the current goal, then proving the rule’s body—is resolution.

Chained resolution

Resolution can chain across multiple rules. Consider an extended knowledge base:

philosopher(socrates).

human(X) :-
    philosopher(X).

mortal(X) :-
    human(X).

and the same query:

?- mortal(socrates).

Prolog again works backward from mortal(socrates). This matches mortal(X) :- human(X) with X bound to socrates, so the goal becomes human(socrates). That matches human(X) :- philosopher(X), reducing the goal further to philosopher(socrates). This is a fact, so the derivation is complete.

The proof tree now has two levels:

\dfrac{\dfrac{\vdash philosopher(socrates)}{\vdash human(socrates)}}{\vdash mortal(socrates)}

Each level corresponds to one application of resolution: the current goal is replaced by the body of the matching rule, working upward until only facts remain.

Resolution and backtracking

When a goal matches more than one clause, Prolog tries them in order. If the first match eventually leads to failure, Prolog backtracks to the choice point and tries the next candidate. This interplay of resolution and backtracking is how Prolog explores the full space of possible derivations and returns multiple answers to a single query.

Consider this knowledge base:

likes(alice, pizza).
likes(alice, sushi).
likes(bob, sushi).
likes(bob, tacos).

Here’s a query which asks whether there is something both Alice and Bob like:

?- likes(alice, X), likes(bob, X).

Prolog attempts to satisfy the conjunction left to right.

Step 1. Prove likes(alice, X). The first matching clause is likes(alice, pizza), so X is bound to pizza. Prolog records a choice point here, noting that likes(alice, sushi) remains as an alternative.

Step 2. Now prove likes(bob, pizza). The only likes/2 facts for bob are likes(bob, sushi) and likes(bob, tacos). Neither unifies with likes(bob, pizza). This subgoal fails.

Step 3. Prolog backtracks to the choice point from Step 1. The binding X = pizza is undone, and Prolog tries the next alternative: likes(alice, sushi). X is now bound to sushi.

Step 4. Prove likes(bob, sushi). This fact is in the knowledge base, and so the goal succeeds.

X = sushi.

The binding X = sushi satisfies both subgoals and Prolog reports the answer. If the user were to request additional solutions (by typing ;), Prolog would backtrack past this success and search for other answers. (In this case, there are none.)

If we enable trace, we can see this quite clearly.

[trace] 100 ?- likes(alice, X), likes(bob, X).
   Call: (13) likes(alice, _20408) ? creep
   Exit: (13) likes(alice, pizza) ? creep
   Call: (13) likes(bob, pizza) ? creep
   Fail: (13) likes(bob, pizza) ? creep
   Redo: (13) likes(alice, _20408) ? creep
   Exit: (13) likes(alice, sushi) ? creep
   Call: (13) likes(bob, sushi) ? creep
   Exit: (13) likes(bob, sushi) ? creep
X = sushi.

The declarative paradigm

Notice that we’ve never had to tell Prolog how to go about this. Were we using in imperative language we’d have to tell our program to iterate over all the things that Alice likes, and for each thing she likes see if Bob likes it too. Or we could generate a set of the things Alice likes and the set of things Bob likes and take the intersection. With Prolog, we need only provide facts and rules, and then we issue queries. Prolog’s inference engine drives resolution, unification, and backtracking, without explicit, imperative instructions.

Copyright © 2023–2026 Clayton Cafiero

No generative AI was used in producing drafts of this material. This was written the old-fashioned way. AI was used to rewrite existing pseudocode in LaTeX to produce standalone *.tex files for rendering, and for revisions toward satisfying WCAG 2.1 AA-level accessibility standards as required by UVM policy. It may also have been used to proofread selected human-written prose. Claude 2.1.150 with model Sonnet 4.6. Revisions, if any, were performed by the author. AI was not used in generating any code whatsoever. All code samples and starter code are by the author only.