Skip to content
  • First-Order Logic & Unification · Artificial Intelligence

First-Order Logic & Unification

LearnViaNotes @learnvianotes· 5 min read

Propositional logic needs a separate symbol for every fact. First-order logic (FOL) adds objects, relations, functions and quantifiers, so one sentence can say something about all objects — “every king is a person”.

ElementExamplesDenotes
Constant symbolsJohn, Richard, 2Objects
Predicate symbolsKing(x), Brother(x, y), >Relations (true/false)
Function symbolsLeftLeg(x), Mother(x), +Functions returning objects
Variablesx, yRange over objects
Quantifiers∀x (for all), ∃x (there exists)Generality
EqualityFather(John) = HenryTwo terms name the same object

Translating English

EnglishFirst-order logic
All kings are persons∀x King(x) ⇒ Person(x)
King John has a crown on his head∃x Crown(x) ∧ OnHead(x, John)
Everybody loves somebody∀x ∃y Loves(x, y)
There is someone who is loved by everyone∃y ∀x Loves(x, y)
Nobody likes taxes¬∃x Likes(x, Taxes) ≡ ∀x ¬Likes(x, Taxes)
Brothers are siblings∀x, y Brother(x, y) ⇒ Sibling(x, y)

Unification

Python
def is_var(t):
    return isinstance(t, str) and t[0].islower()        # variables: lowercase; constants: Capitalised

def substitute(t, theta):
    if is_var(t):
        return substitute(theta[t], theta) if t in theta else t
    if isinstance(t, tuple):
        return tuple(substitute(a, theta) for a in t)
    return t

def occurs(var, t, theta):
    t = substitute(t, theta)
    return t == var or (isinstance(t, tuple) and any(occurs(var, a, theta) for a in t))

def unify(x, y, theta):
    if theta is None:
        return None
    x, y = substitute(x, theta), substitute(y, theta)
    if x == y:
        return theta
    if is_var(x):
        return None if occurs(x, y, theta) else {**theta, x: y}      # occurs check
    if is_var(y):
        return unify(y, x, theta)
    if isinstance(x, tuple) and isinstance(y, tuple) and len(x) == len(y):
        for a, b in zip(x, y):
            theta = unify(a, b, theta)
        return theta
    return None

def show(theta):
    return theta if theta is None else {v: substitute(v, theta) for v in theta}

q = ("Knows", "John", "x")
print(show(unify(q, ("Knows", "John", "Jane"), {})))               # {'x': 'Jane'}
print(show(unify(q, ("Knows", "y", "Bill"), {})))                  # {'y': 'John', 'x': 'Bill'}
print(show(unify(q, ("Knows", "y", ("Mother", "y")), {})))         # {'y': 'John', 'x': ('Mother', 'John')}
print(show(unify(q, ("Knows", "x", "Elizabeth"), {})))             # None ← x can't be John and Elizabeth
print(show(unify(q, ("Knows", "x2", "Elizabeth"), {})))            # {'x2': 'John', 'x': 'Elizabeth'} ← fixed by standardising apart

Inference in FOL

Generalised modus ponens lifts modus ponens with unification: from King(John), Greedy(y) and ∀x King(x) ∧ Greedy(x) ⇒ Evil(x), unify to θ = {x/John, y/John} and infer Evil(John) — only if Greedy(John) also unifies. Forward chaining with it gives Datalog-style reasoning. The classic crime KB:

Python
FACTS = {("American", "West"), ("Missile", "M1"), ("Owns", "Nono", "M1"), ("Enemy", "Nono", "America")}
RULES = [
    ([("American", "x"), ("Weapon", "y"), ("Sells", "x", "y", "z"), ("Hostile", "z")], ("Criminal", "x")),
    ([("Missile", "x"), ("Owns", "Nono", "x")], ("Sells", "West", "x", "Nono")),
    ([("Missile", "x")], ("Weapon", "x")),
    ([("Enemy", "x", "America")], ("Hostile", "x")),
]

def match(pattern, fact, theta):                   # one-way unification: variables only in the pattern
    if len(pattern) != len(fact) or pattern[0] != fact[0]:
        return None
    theta = dict(theta)
    for p, f in zip(pattern[1:], fact[1:]):
        if p[0].islower():
            if theta.setdefault(p, f) != f:
                return None
        elif p != f:
            return None
    return theta

def solutions(premises, facts, theta):
    if not premises:
        yield theta
        return
    for fact in sorted(facts):
        t = match(premises[0], fact, theta)
        if t is not None:
            yield from solutions(premises[1:], facts, t)

def forward_chain(facts, rules):
    facts, rounds = set(facts), []
    while True:
        new = {tuple(theta.get(a, a) for a in concl)
               for prem, concl in rules for theta in solutions(prem, facts, {})} - facts
        if not new:
            return facts, rounds
        rounds.append(sorted(new))
        facts |= new

facts, rounds = forward_chain(FACTS, RULES)
print(rounds[0])                                   # [('Hostile', 'Nono'), ('Sells', 'West', 'M1', 'Nono'), ('Weapon', 'M1')]
print(rounds[1])                                   # [('Criminal', 'West')]
print(("Criminal", "West") in facts)               # True
MethodHowUsed in
Forward chainingGeneralised modus ponens from facts until nothing newDatalog, production systems, deductive databases
Backward chainingDepth-first from the goal, unifying with rule headsProlog, logic programming
ResolutionConvert to clausal form (Skolemise ∃), resolve with unificationAutomated theorem provers (Vampire, E)

Why is first-order logic more expressive than propositional logic?

What does unification do? Give an example.

What is the occurs check?

Free for students and teachers

Write notes like this one

Cards for every idea, quizzes and flashcards, read aloud — on the web and your phone.

Something wrong with this note? Open it to report