Facts & Rules

Course 1 · Ch 2
Facts & Rules
Multiple clauses, one predicate — genuinely comparable to haskell1-4's own multiple-equations-one-function shape

prolog1-1 covered plain facts. This chapter adds rules — facts that depend on other goals being true — and gets precise about what actually organizes a Prolog program.

Rules — Facts That Depend on Other Facts

grandparent(X, Z) :- parent(X, Y), parent(Y, Z).

:- reads as "if." This rule says: grandparent(X, Z) is true if parent(X, Y) is true and parent(Y, Z) is true. The comma is logical AND — a genuinely different reading from every other language's own comma usage on this site.

The Body Is Just More Goals

A rule's body is itself a sequence of goals, each one potentially a plain fact or another rule. Rules can call other rules — genuinely comparable in spirit to function composition — but querying a rule works exactly the same way as querying a plain fact. The caller can't tell the difference from outside at all, a real, clean uniformity.

Predicates — The Real Unit of Organization

parent(tom, bob). parent(tom, liz). -- both facts above, TOGETHER, define one predicate: parent/2

A predicate is a name plus arity (argument count) — parent/2. Every fact and rule sharing that same name and argument count together define one predicate. Genuinely comparable to how haskell1-4's own multiple function equations (sumList's base case and recursive case) together define one function — Prolog's version has this exact "multiple clauses, one definition" shape.

Recursive Rules — Building ancestor from parent

ancestor(X, Y) :- parent(X, Y). -- base case ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y). -- recursive case

A real, genuinely recursive rule — base case plus recursive case, directly comparable to haskell1-4's own base-case/recursive-case shape for functions. The difference: this recursively defines a relation, not a function computing a single value.

Variables Are Scoped to a Single Clause

ancestor(X, Y) :- parent(X, Y). ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y). -- the X in line 1 has NOTHING to do with the X in line 2 — completely fresh variables each clause

A real, easy-to-miss detail: a variable's name is scoped to a single clause only. Reusing X across two clauses of the same predicate does not link them — each clause gets its own completely independent set of variables, a real departure from ordinary lexical scoping in every other language covered so far.

Anonymous Variables — The Underscore

likes(mary, _). -- "mary likes something" — the something is never bound to a name

_ means "some value exists here, but I don't care what it is, and I won't bind it to a name." Directly comparable to haskell1-4's and haskell1-7's own discard pattern — a real, genuine syntactic and conceptual convergence between the two languages.

AspectHaskell (haskell1-4/haskell1-8)Prolog
Multiple definitions, one unitmultiple function equationsmultiple clauses (facts/rules) of one predicate
Discard/don't-care binding_ in a pattern_ as an anonymous variable
What defines "the same unit"function namename + arity together (parent/2)
Think and document in terms of name/arity
parent/2 and a hypothetical parent/3 would be two completely separate, unrelated predicates in Prolog, despite sharing a name — get in the habit of naming predicates by their arity too when thinking or writing about them.
Reusing a variable name across clauses does not link them
Writing X in two different clauses of the same predicate looks like it might connect them — it doesn't. Each clause's variables are completely independent, a genuine, real source of early confusion coming from languages where a reused name usually means something.

Coding Challenges

Challenge 1

Write parent/2 facts for a small three-generation family, then write a grandparent/2 rule using two parent/2 goals, and query it for a specific grandparent-grandchild pair.

📄 View solution
Challenge 2

Write a recursive ancestor/2 predicate (base case + recursive case) using your own parent/2 facts, and query it for an ancestor relationship that spans three generations, requiring the recursive case to fire.

📄 View solution
Challenge 3

Write a short comment explaining why writing X in two separate clauses of the same predicate does NOT create any connection between them, and what a programmer should do instead if they genuinely need to compare or relate two values across separate parts of a rule.

📄 View solution

Chapter 2 Quick Reference

  • :- reads as "if"; a comma in a rule body is logical AND
  • A rule's body is just more goals — calling a rule looks identical to calling a plain fact from outside
  • A predicate is a name+arity pair (parent/2) — every fact/rule sharing both together defines one predicate, the same "multiple clauses, one unit" shape as haskell1-4's own function equations
  • Recursive rules (ancestor/2) use the same base-case/recursive-case shape as haskell1-4's own recursive functions, but define a relation rather than compute a value
  • Variables are scoped to a single clause — the same name in two clauses is NOT connected
  • _ is the anonymous variable — "something exists, I don't care what," directly comparable to haskell1-4's/haskell1-7's own discard pattern
  • Next chapter: unification — Prolog's real central mechanism, contrasted directly with Haskell's own pattern matching