1

Facts and Queries

Why this matters

You already know Python, JavaScript, and C++. In all three, you tell the computer how to solve a problem — step by step, with loops, variables, and function calls. Prolog is fundamentally different: you describe what is true, and Prolog figures out the answers. This shift from imperative recipes to declarative descriptions is the single most important idea in this tutorial — and the foundation for everything that follows.

🎯 You will learn to

  • Apply Prolog facts to record relationships in a knowledge base
  • Analyze query results to see how the same predicate works in multiple directions

Facts: Declaring what is true

A fact states something unconditionally:

parent(tom, bob).

This says: “Tom is a parent of Bob.” Every fact ends with a period.

There is no return statement. No variables being assigned. You are simply recording a relationship.

Queries: Asking questions

Use the ?- input to ask Prolog questions:

Query Meaning Expected answer
parent(tom, bob) “Is Tom a parent of Bob?” true
parent(tom, X) “Who are Tom’s children?” X = bob
parent(X, bob) “Who are Bob’s parents?” X = tom

Notice that the same predicate works in multiple directions. This is not a function with fixed inputs and outputs — it’s a relation that Prolog can query from any angle.

Imperative trap: In Python, parent(tom, bob) would be a function call returning a value. In Prolog, it’s a question — and the answer is yes/no, plus any variable bindings.

Your Task

The editor has one fact. Add two more:

  1. parent(bob, ann).
  2. parent(bob, pat).

Then try these queries (one at a time in the ?- input):

  • parent(tom, bob) — yes/no question
  • parent(tom, X) — “who” question
  • parent(X, ann) — reverse direction!
  • parent(X, Y) — enumerate all parent relationships
Starter files
family.pl
% Family database
% A fact parent(X, Y) means "X is a parent of Y".

parent(tom, bob).

% TODO: Add two more facts:
% parent(bob, ann).
% parent(bob, pat).
2

Unification: = Is Not Assignment

Why this matters

In Python, X = 5 stores the value 5 in a variable named X. In Prolog, X = 5 means something completely different — it’s bidirectional pattern matching, not assignment. Mis-reading = as assignment is the single most common source of confusion when programmers transfer in from imperative languages, and it will silently break any code that uses arithmetic, structures, or recursion. Get this concept right and the rest of Prolog falls into place.

🎯 You will learn to

  • Analyze whether two Prolog terms unify and what bindings result
  • Evaluate why X = 3, X = 4 fails (single-assignment) where Python would simply reassign

Unification: bidirectional pattern matching

The = operator in Prolog asks: “Can these two terms be made identical?”

?- X = 5.          % Yes: X becomes 5
?- 5 = 5.          % Yes: identical
?- 5 = 6.          % No: cannot make 5 equal to 6
?- f(X) = f(3).    % Yes: X becomes 3
?- f(X, b) = f(a, Y).  % Yes: X = a, Y = b

This is called unification — the engine of Prolog. It works by matching the structure of two terms and finding variable bindings that make them equal.

Variables are NOT mutable boxes

Imperative trap: In Python, you can write x = 3 then x = 4. In Prolog, once a variable is bound, it cannot be changed:

?- X = 3, X = 4.   % FAILS! X is already 3, and 3 ≠ 4.

This is called single assignment — a variable gets one value per query. If you need a different value, you need a different variable or a different branch of the search.

Unification with structures

Prolog terms can be nested structures (like trees):

?- date(Day, march, 2024) = date(15, Month, Year).
% Day = 15, Month = march, Year = 2024

Both sides are made identical by binding variables to values. Neither side is “input” or “output” — unification is symmetric.

Your Task

The file contains several unification queries as facts. For each one, predict whether it will succeed or fail, then uncomment it and run it to check.

Starter files
unify.pl
% Unification exercises
% For each, PREDICT: will it succeed or fail? What are the bindings?
% Then uncomment and query: test1, test2, etc.

% Example: X = hello succeeds with X = hello
test_example :- X = hello, write('X = '), write(X), nl.

% 1. Does this succeed? What is X?
test1 :- X = f(a, b), write('X = '), write(X), nl.

% 2. Does this succeed? What are X and Y?
test2 :- f(X, b) = f(a, Y), write('X = '), write(X), write(', Y = '), write(Y), nl.

% 3. Does this succeed or FAIL?
test3 :- a = b, write('This should not print'), nl.

% 4. Does this succeed or FAIL? (tricky!)
test4 :- X = 3, X = 3, write('X = '), write(X), nl.

% 5. Does this succeed or FAIL?
test5 :- X = 3, X = 4, write('This should not print'), nl.

% Run all tests:
test_unify :-
  write('--- Test 1 ---'), nl, (test1 -> true ; write('FAILED'), nl),
  write('--- Test 2 ---'), nl, (test2 -> true ; write('FAILED'), nl),
  write('--- Test 3 ---'), nl, (test3 -> true ; write('FAILED'), nl),
  write('--- Test 4 ---'), nl, (test4 -> true ; write('FAILED'), nl),
  write('--- Test 5 ---'), nl, (test5 -> true ; write('FAILED'), nl).
3

Rules: Deriving New Knowledge

Why this matters

Facts let you record what you already know. Rules let Prolog derive new knowledge you never wrote down — relationships like grandparent, ancestor, or sibling follow logically from the facts you have. This is where Prolog earns its keep over a database: you write the logic, and Prolog handles the search for matching values. No if/else, no loops — just declarative implications.

🎯 You will learn to

  • Apply the :- operator to define rules that compose existing predicates
  • Analyze how Prolog searches for variable bindings that satisfy a rule’s body

Rule syntax

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

Read :- as “if” and , as “and”:

X is a grandparent of Z if X is a parent of some Y and Y is a parent of Z.

The left side is the head (what we’re defining). The right side is the body (the conditions).

How Prolog uses rules

When you query grandparent(tom, ann), Prolog:

  1. Finds the rule for grandparent
  2. Tries to satisfy the body: find a Y where parent(tom, Y) AND parent(Y, ann)
  3. Tries Y = bob: is parent(tom, bob) true? Yes. Is parent(bob, ann) true? Yes.
  4. Both conditions met — the query succeeds.

Imperative trap: There is no if/else, no loop. Prolog tries all possibilities automatically. You declare the logic, and Prolog handles the search.

Variables in rules

Variables in rules (like X, Y, Z) are universally quantified — they stand for any value that makes the rule work. When Prolog uses a rule, it creates fresh copies of all variables for that specific use.

Your Task

Define two rules:

  1. grandparent(X, Z) — as shown above
  2. sibling(X, Y) — X and Y are siblings if they share a parent, but are not the same person. Use X \= Y for “not equal”.
Starter files
family.pl
% Family database
parent(tom, bob).
parent(bob, ann).
parent(bob, pat).

% Rule: grandparent
% TODO: Define grandparent(X, Z) :- ...


% Rule: sibling
% Two people are siblings if they share a parent
% but are not the same person.
% TODO: Define sibling(X, Y) :- ...
4

Backtracking: The Search for Solutions

Why this matters

When Prolog encounters a query, it doesn’t just find one answer — it systematically searches for all answers, exploring every combination of variable bindings. This mechanism, called backtracking, is what replaces nested for loops, list comprehensions, and explicit search code in imperative languages. Once you can trace backtracking by hand, you can predict exactly which solutions Prolog returns and in what order — a prerequisite for writing correct recursive predicates later.

🎯 You will learn to

  • Analyze a query trace step by step to predict which solutions Prolog returns and in what order
  • Apply backtracking to enumerate all combinations of values across multiple predicates

The search process

Consider this knowledge base:

food(pizza).
food(pasta).
food(sushi).
drink(water).
drink(juice).
meal(F, D) :- food(F), drink(D).

Query: meal(X, Y) — “What are all possible meals?”

Prolog’s search:

  1. Try food(pizza) — success! Now try drink(water) — success! Answer: X=pizza, Y=water
  2. Backtrack to try next drink: drink(juice) — success! Answer: X=pizza, Y=juice
  3. No more drinks. Backtrack to try next food: food(pasta), then try drinks again…
  4. Continue until all combinations are exhausted.

Result: 6 answers (3 foods x 2 drinks).

Imperative trap: In Python, you’d write nested for loops. In Prolog, the “looping” happens automatically through backtracking. You never write loop syntax.

Controlling backtracking with ; (or)

The ; operator means “or”:

pet(X) :- cat(X) ; dog(X).

X is a pet if X is a cat OR X is a dog.

Your Task

The file defines a small meal planning database. Define the meal/2 predicate, then explore how backtracking produces multiple solutions.

Try these queries:

  • meal(X, Y) — all possible meals
  • meal(pizza, Y) — what can you drink with pizza?
  • meal(X, water) — what food goes with water?
Starter files
meals.pl
% Foods and drinks
food(pizza).
food(pasta).
food(sushi).

drink(water).
drink(juice).

% TODO: Define meal(F, D) - a meal is a food paired with a drink
% meal(F, D) :- ...


% Bonus: Define healthy_meal/2 where the food is sushi
% and the drink is water.
% healthy_meal(F, D) :- ...
5

Arithmetic: is vs =

Why this matters

Arithmetic in Prolog has a critical gotcha that trips up every imperative programmer: X = 2 + 3 does not compute 5 — it binds X to the unevaluated structure +(2, 3). To actually compute, you need the is/2 operator. Mixing these up produces predicates that silently bind variables to terms instead of numbers, leading to confusing failures further down the chain. Knowing exactly when to use = vs is is the gate to writing any Prolog program that touches numbers.

🎯 You will learn to

  • Apply is/2 to evaluate arithmetic expressions and bind the result to a variable
  • Evaluate when to use =, is, and =:= for the right kind of comparison or assignment

The trap: = does NOT evaluate

?- X = 2 + 3.
% X = 2+3    (NOT 5! It's the structure +(2,3))

Remember: = is unification. It makes two terms structurally identical. 2 + 3 is a structure (like a tree node with + as the functor), not a computation.

The solution: is/2 forces evaluation

?- X is 2 + 3.
% X = 5      (evaluates the right side, then unifies)

is evaluates the arithmetic expression on the right side, then unifies the result with the left side. The right side must be fully instantiated (no unbound variables).

Comparison operators

Operator Meaning Example
=:= Arithmetic equality 2+3 =:= 5 succeeds
=\= Arithmetic inequality 2+3 =\= 6 succeeds
< Less than 3 < 5 succeeds
> Greater than 5 > 3 succeeds
>= Greater or equal 5 >= 5 succeeds
=< Less or equal 3 =< 5 succeeds

Note: =< not <=! This is a common Prolog syntax surprise.

Your Task

Define these predicates:

  1. bmi(Height, Weight, BMI) — BMI is Weight / Height^2. Use is to compute it.
  2. is_overweight(Height, Weight) — succeeds if BMI > 25.
  3. abs_diff(X, Y, D) — D is the absolute difference between X and Y.
Starter files
arithmetic.pl
% BMI calculator
% bmi(Height, Weight, BMI) - compute BMI = Weight / Height^2
% Hint: use ** for exponentiation: Height ** 2
% TODO: bmi(Height, Weight, BMI) :- BMI is ...


% is_overweight(Height, Weight) - succeeds if BMI > 25
% TODO: is_overweight(Height, Weight) :- ...


% abs_diff(X, Y, D) - D is the absolute difference |X - Y|
% Hint: use two clauses, one for X >= Y and one for X < Y
% TODO: abs_diff(X, Y, D) :- ...
6

Lists and Pattern Matching

Why this matters

Lists are Prolog’s fundamental container — almost every nontrivial Prolog program processes them. The [H|T] destructuring pattern is your primary tool for working with lists, and unlike Python’s slicing or JS array methods, it works bidirectionally: the same pattern that decomposes a list can also build one. Mastering head/tail patterns now (without recursion yet) makes the recursive predicates in the next step feel natural instead of magical.

🎯 You will learn to

  • Apply [H|T] patterns to destructure a list into head and tail
  • Analyze nested patterns like [X, Y | Rest] to grab specific positions without writing code
    [a, b, c]        % a list of three atoms
    [1, 2, 3]        % a list of three numbers
    []               % the empty list
    

But how you work with them is very different from Python/JS.

Head|Tail decomposition

The [H|T] pattern splits a list into its first element (head) and the rest (tail):

?- [H|T] = [a, b, c].
% H = a, T = [b, c]

This is Prolog’s equivalent of destructuring — but powered by unification:

?- [X, Y | Rest] = [1, 2, 3, 4, 5].
% X = 1, Y = 2, Rest = [3, 4, 5]

Transfer from Python: This is similar to Python’s head, *tail = [1, 2, 3]. But in Prolog, it works bidirectionally — you can also build lists this way.

Writing list predicates (non-recursive)

% first(X, List) - X is the first element of List
first(X, [X|_]).

% second(X, List) - X is the second element
second(X, [_, X|_]).

The underscore _ means “I don’t care about this value.” Each _ is independent.

Your Task

Define these predicates using pattern matching (no recursion needed):

  1. head(X, List) — X is the first element of List
  2. tail(T, List) — T is the tail (everything except the first element)
  3. has_two([_, _]) — succeeds only if the list has exactly two elements
  4. swap_first_two(List, Swapped) — swap the first two elements: [a,b,c] becomes [b,a,c]
Starter files
lists.pl
% head(X, List) - X is the first element
% TODO: head(X, [... | ...]).


% tail(T, List) - T is everything after the first element
% TODO: tail(T, [... | ...]).


% has_two(List) - succeeds only if List has exactly 2 elements
% Hint: use the pattern [_, _]
% TODO: has_two(...).


% swap_first_two(List, Swapped) - swap first two elements
% [a, b | Rest] becomes [b, a | Rest]
% TODO: swap_first_two([... | ...], [... | ...]).
7

Recursive List Processing

Why this matters

Prolog has no for or while loops — recursion is the only way to process lists of arbitrary length. Every recursive predicate follows the same skeleton: a base case for the empty list and a recursive case that strips the head and recurses on the tail. Once you internalize this base-case/recursive-case pattern, you can write member, length, append, reverse, map, filter, and most other list operations almost mechanically.

🎯 You will learn to

  • Apply the base-case/recursive-case pattern to write recursive list predicates
  • Create new predicates (my_last, my_append, my_reverse) by following a worked example

The pattern: Base case + Recursive case

Every recursive predicate has two clauses:

% Sub-goal: Base case — empty list
my_length([], 0).

% Sub-goal: Recursive case — strip head, recurse on tail
my_length([_|T], N) :- my_length(T, N1), N is N1 + 1.

Transfer from Python: This is like:

def my_length(lst):
    if lst == []:       # base case
        return 0
    return 1 + my_length(lst[1:])  # recursive case

The key difference: Prolog’s version works in both directions!

Worked example: my_member/2

% Sub-goal: Base case — X is the head of the list
my_member(X, [X|_]).

% Sub-goal: Recursive case — X is in the tail
my_member(X, [_|T]) :- my_member(X, T).

Try my_member(b, [a, b, c]) — succeeds. Try my_member(X, [a, b, c]) — returns all elements!

Your Task

Using the my_member worked example as a model:

  1. my_last(X, List) — X is the last element of List
    • Base case: the last element of a one-element list [X] is X
    • Recursive case: the last element of [_|T] is the last element of T
  2. my_append(L1, L2, L3) — L3 is L1 concatenated with L2
    • Base case: appending L to [] gives L
    • Recursive case: appending [H|T1] to L2 gives [H|T3] where T3 is T1 appended to L2
  3. my_reverse(List, Reversed) — reverse a list
    • Base case: reverse of [] is []
    • Recursive case: reverse of [H|T] is reverse of T appended with [H]
    • Hint: use your my_append predicate!
Starter files
recursion.pl
% --- Worked example (provided) ---

% my_member(X, List) - X is an element of List
% Sub-goal: Base case — X is the head
my_member(X, [X|_]).
% Sub-goal: Recursive case — X is in the tail
my_member(X, [_|T]) :- my_member(X, T).


% --- Your turn: follow the same pattern ---

% my_last(X, List) - X is the last element of List
% Sub-goal: Base case — last of a one-element list
% TODO: my_last(X, [X]).
% Sub-goal: Recursive case — skip head, recurse on tail
% TODO: my_last(X, [_|T]) :- my_last(X, T).


% my_append(L1, L2, L3) - L3 is L1 concatenated with L2
% Sub-goal: Base case — appending to empty list
% TODO: my_append([], L, L).
% Sub-goal: Recursive case — move head to result
% TODO: my_append([H|T1], L2, [H|T3]) :- my_append(T1, L2, T3).


% my_reverse(List, Reversed) - reverse a list
% Sub-goal: Base case — reverse of empty is empty
% TODO: my_reverse([], []).
% Sub-goal: Recursive case — reverse tail, append head
% TODO: my_reverse([H|T], R) :- my_reverse(T, RT), my_append(RT, [H], R).
8

Putting It All Together

Why this matters

Individual pieces — facts, rules, arithmetic, lists, recursion — are useful, but real Prolog programs combine them. This synthesis step has you compose every concept from the tutorial into a small grade analyzer that looks up data, computes statistics, and answers multi-directional queries. Building one program end-to-end is what consolidates the pieces into a working mental model you can transfer to your own problems.

🎯 You will learn to

  • Create a multi-predicate Prolog program that composes facts, rules, arithmetic, and recursion
  • Evaluate when a problem is well-suited to declarative logic programming

Challenge: Student Grade Analyzer

Build a knowledge base that stores student grades and provides useful queries:

Facts (provided):

student(alice, [88, 92, 75, 95]).
student(bob,   [70, 65, 80, 72]).
student(carol, [95, 98, 92, 97]).

Each student has a name and a list of exam scores.

Your task — define these predicates:

  1. sum_list(List, Sum) — compute the sum of a list of numbers
    • Base: sum of [] is 0
    • Recursive: sum of [H|T] is H plus the sum of T
  2. length_list(List, Len) — compute the length of a list
    • (Same pattern as my_length from Step 6)
  3. average(Name, Avg) — a student’s average score
    • Look up the student’s scores, compute sum and length, divide
  4. above_average(Name, Threshold) — succeeds if the student’s average is above Threshold

Then query:

  • average(alice, A) — What is Alice’s average?
  • above_average(carol, 90) — Does Carol average above 90?
  • above_average(Name, 80) — Which students average above 80?
Starter files
grades.pl
% Student grade database
student(alice, [88, 92, 75, 95]).
student(bob,   [70, 65, 80, 72]).
student(carol, [95, 98, 92, 97]).

% sum_list(List, Sum) - sum of all numbers in List
% Sub-goal: Base case
% TODO: sum_list([], 0).
% Sub-goal: Recursive case
% TODO: sum_list([H|T], Sum) :- sum_list(T, Rest), Sum is H + Rest.


% length_list(List, Len) - number of elements in List
% Sub-goal: Base case
% TODO: length_list([], 0).
% Sub-goal: Recursive case
% TODO: length_list([_|T], Len) :- length_list(T, L1), Len is L1 + 1.


% average(Name, Avg) - compute a student's average score
% TODO: average(Name, Avg) :- ...


% above_average(Name, Threshold) - true if average > Threshold
% TODO: above_average(Name, Threshold) :- ...