Prolog Essentials
A hands-on introduction to logic programming with Prolog, running entirely in your browser. Learn to think declaratively — define facts, write rules, and let Prolog find the answers.
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:
parent(bob, ann).parent(bob, pat).
Then try these queries (one at a time in the ?- input):
parent(tom, bob)— yes/no questionparent(tom, X)— “who” questionparent(X, ann)— reverse direction!parent(X, Y)— enumerate all parent relationships
% 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).
Solution
% Family database
% A fact parent(X, Y) means "X is a parent of Y".
parent(tom, bob).
parent(bob, ann).
parent(bob, pat).
Why this works:
- Each fact is a simple declaration:
parent(bob, ann).tells Prolog that Bob is a parent of Ann. There is no function call, no return value — just a statement of truth. - The period
.at the end is mandatory — it terminates every Prolog clause (fact, rule, or query). - Once these facts are in the knowledge base, Prolog can answer queries from any direction:
parent(bob, X)finds Bob’s children,parent(X, ann)finds Ann’s parents, andparent(X, Y)lists all parent-child pairs. This multi-directionality is what makes Prolog predicates relations, not functions.
Step 1 — Knowledge Check
Min. score: 80%1. Given these facts:
likes(alice, pizza).
likes(bob, pasta).
likes(alice, sushi).
likes(alice, X) return?
Prolog finds all facts that match the query pattern. Since Alice likes both pizza and sushi, both are returned. This is fundamentally different from a function call that returns one value.
2. In Python, parent(tom, bob) is a function call. In Prolog, it is:
Prolog predicates are relations, not functions. parent(tom, bob) asks: “does the knowledge base contain this relationship?” There is no return value — Prolog either succeeds or fails.
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 = 4fails (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 = 3thenx = 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.
% 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).
Solution
% Unification exercises — all uncommented
test_example :- X = hello, write('X = '), write(X), nl.
% 1. Succeeds: X = f(a, b)
test1 :- X = f(a, b), write('X = '), write(X), nl.
% 2. Succeeds: X = a, Y = b
test2 :- f(X, b) = f(a, Y), write('X = '), write(X), write(', Y = '), write(Y), nl.
% 3. FAILS: a and b are different atoms — cannot unify
test3 :- a = b, write('This should not print'), nl.
% 4. Succeeds: X = 3, and 3 = 3 trivially unifies
test4 :- X = 3, X = 3, write('X = '), write(X), nl.
% 5. FAILS: X = 3, then X = 4 tries to unify 3 with 4
test5 :- X = 3, X = 4, write('This should not print'), nl.
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).
Key insights:
- Test 1:
X = f(a, b)succeeds — X is bound to the structuref(a, b). Unification works with any term, not just numbers. - Test 2:
f(X, b) = f(a, Y)matches corresponding arguments: X witha,bwith Y. Both sides becomef(a, b). - Test 3 FAILS:
a = btries to make two different atoms identical — impossible. This is the core of unification: it only succeeds when terms can be made equal. - Test 4 SUCCEEDS:
X = 3, X = 3— after X is bound to 3, unifying X with 3 again just checks that 3 = 3 (trivially true). - Test 5 FAILS:
X = 3, X = 4— X is bound to 3, soX = 4becomes3 = 4, which fails. Variables are single-assignment — this is the #1 difference from imperative languages.
Step 2 — Knowledge Check
Min. score: 80%
1. What happens when Prolog processes X = 3, X = 4?
Once X is bound to 3, X = 4 attempts to unify 3 with 4, which fails. Prolog variables are single-assignment within a query. This is the #1 source of confusion for imperative programmers.
2. Does point(X, 2) = point(1, Y) succeed? If so, what are X and Y?
Unification matches corresponding arguments: X unifies with 1, and 2 unifies with Y. Both sides become point(1, 2). Prolog doesn’t need point to be “defined” — it’s just a structure.
3. How does Prolog’s = differ from Python’s =?
Python’s = always assigns the right side to the left. Prolog’s = finds bindings that make both sides identical — it works symmetrically. f(X) = f(3) and f(3) = f(X) both bind X to 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:
- Finds the rule for
grandparent - Tries to satisfy the body: find a Y where
parent(tom, Y)ANDparent(Y, ann) - Tries
Y = bob: isparent(tom, bob)true? Yes. Isparent(bob, ann)true? Yes. - 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:
grandparent(X, Z)— as shown abovesibling(X, Y)— X and Y are siblings if they share a parent, but are not the same person. UseX \= Yfor “not equal”.
% 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) :- ...
Solution
% Family database
parent(tom, bob).
parent(bob, ann).
parent(bob, pat).
% Rule: grandparent
grandparent(X, Z) :- parent(X, Y), parent(Y, Z).
% Rule: sibling
sibling(X, Y) :- parent(P, X), parent(P, Y), X \= Y.
Why this works:
grandparent(X, Z) :- parent(X, Y), parent(Y, Z).— Read as: “X is a grandparent of Z if there exists some Y such that X is a parent of Y and Y is a parent of Z.” The variable Y is an existential — Prolog searches for any value that makes both conditions true.sibling(X, Y) :- parent(P, X), parent(P, Y), X \= Y.— Two people share a parent P, butX \= Yensures they aren’t the same person. Without this check,sibling(ann, ann)would incorrectly succeed (bob is a parent of ann AND ann).- No loops, no
if/else: Prolog checks all possibilities automatically via backtracking. When you querysibling(X, Y), Prolog tries every combination of P, X, Y that satisfies all three conditions.
Step 3 — Knowledge Check
Min. score: 80%
1. Given parent(tom, bob) and parent(bob, ann), what does grandparent(tom, ann) do?
Even though there is no fact grandparent(tom, ann), Prolog uses the rule to derive it. It finds Y = bob that satisfies both parent(tom, bob) and parent(bob, ann). Rules allow Prolog to answer questions that aren’t explicitly stated.
2. If we define sibling(X, Y) :- parent(P, X), parent(P, Y). (without X \= Y), what happens when we query sibling(ann, ann)?
Without the X \= Y check, ann would be her own sibling (bob is a parent of ann AND ann). This is logically correct but semantically wrong — we need the inequality constraint to exclude self-siblings.
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:
- Try
food(pizza)— success! Now trydrink(water)— success! Answer: X=pizza, Y=water - Backtrack to try next drink:
drink(juice)— success! Answer: X=pizza, Y=juice - No more drinks. Backtrack to try next food:
food(pasta), then try drinks again… - Continue until all combinations are exhausted.
Result: 6 answers (3 foods x 2 drinks).
Imperative trap: In Python, you’d write nested
forloops. 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 mealsmeal(pizza, Y)— what can you drink with pizza?meal(X, water)— what food goes with water?
% 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) :- ...
Solution
% Foods and drinks
food(pizza).
food(pasta).
food(sushi).
drink(water).
drink(juice).
% A meal is a food paired with a drink
meal(F, D) :- food(F), drink(D).
% A healthy meal: sushi with water
healthy_meal(F, D) :- meal(F, D), F = sushi, D = water.
Why this works:
meal(F, D) :- food(F), drink(D).— A meal is any food-drink combination. Prolog’s backtracking systematically tries every food with every drink, producing 3 x 2 = 6 answers. This is equivalent to nestedforloops in Python, but you never write loop syntax.healthy_meal(F, D)adds constraints on top ofmeal: the food must be sushi AND the drink must be water. Only one combination survives.- Multi-directional use:
meal(pizza, Y)finds drinks for pizza,meal(X, water)finds foods for water,meal(X, Y)finds all pairs. Same predicate, different query directions.
Step 4 — Knowledge Check
Min. score: 80%
1. How many answers does meal(X, Y) produce, given 3 foods and 2 drinks?
Prolog tries every combination: each food with each drink. With 3 foods and 2 drinks, that’s 3 x 2 = 6 meals. This is like nested for-loops, but Prolog does it automatically through backtracking.
2. In Python, to get all food-drink pairs you’d write:
for f in foods:
for d in drinks:
print(f, d)
In Prolog, you define the relationship (a meal is a food plus a drink), then query it. Prolog’s backtracking mechanism produces all combinations automatically. No explicit loops needed.
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/2to 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:
bmi(Height, Weight, BMI)— BMI is Weight / Height^2. Useisto compute it.is_overweight(Height, Weight)— succeeds if BMI > 25.abs_diff(X, Y, D)— D is the absolute difference between X and Y.
% 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) :- ...
Solution
% BMI calculator
bmi(Height, Weight, BMI) :- BMI is Weight / (Height ** 2).
% is_overweight: succeeds if BMI > 25
is_overweight(Height, Weight) :- bmi(Height, Weight, BMI), BMI > 25.
% abs_diff: absolute difference using two clauses
abs_diff(X, Y, D) :- X >= Y, D is X - Y.
abs_diff(X, Y, D) :- X < Y, D is Y - X.
Why this works:
BMI is Weight / (Height ** 2)— Theisoperator evaluates the arithmetic expression on the right and unifies the result with BMI. Using=instead would give you the unevaluated structure/(Weight, **(Height, 2))— not a number!is_overweightcomposes predicates: It callsbmi/3to compute the BMI, then checksBMI > 25. This is the Prolog way of “calling a function then using its result.”abs_diffuses two clauses: Instead of anif/else, Prolog tries the first clause (X >= Y). If it fails (becauseX < Y), Prolog backtracks and tries the second clause. Multiple clauses for the same predicate is Prolog’s version of pattern matching / case analysis.
Step 5 — Knowledge Check
Min. score: 80%
1. What does X = 2 + 3 bind X to?
The = operator is unification, not evaluation. It makes X structurally identical to 2 + 3, which is a compound term +(2, 3). To get 5, use X is 2 + 3.
2. What happens with X is Y + 1 when Y is unbound (not yet assigned a value)?
The is operator requires the right-hand side to be a ground arithmetic expression (all variables must be bound to numbers). If Y is unbound, Prolog throws an instantiation error.
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):
head(X, List)— X is the first element of Listtail(T, List)— T is the tail (everything except the first element)has_two([_, _])— succeeds only if the list has exactly two elementsswap_first_two(List, Swapped)— swap the first two elements:[a,b,c]becomes[b,a,c]
% 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([... | ...], [... | ...]).
Solution
% head(X, List) - X is the first element
head(X, [X|_]).
% tail(T, List) - T is everything after the first element
tail(T, [_|T]).
% has_two(List) - succeeds only if List has exactly 2 elements
has_two([_, _]).
% swap_first_two(List, Swapped) - swap first two elements
swap_first_two([A, B | Rest], [B, A | Rest]).
Why this works:
head(X, [X|_]).— The pattern[X|_]unifies X with the head of the list. The_discards the tail. This is pure pattern matching — no code body needed.tail(T, [_|T]).— The same variableTappears in both the argument and the pattern. Unification makes them equal, so T is bound to the tail.has_two([_, _]).— The pattern[_, _]only matches lists with exactly two elements. One element gives[_](no match), three gives[_, _, _|_](no match).swap_first_two([A, B | Rest], [B, A | Rest]).— Destructures the first two elements, then reconstructs with them swapped.Restis shared — the tail stays unchanged. No recursion needed; unification does all the work.
Step 6 — Knowledge Check
Min. score: 80%
1. What does [H|T] = [42] bind H and T to?
A single-element list [42] is really [42|[]]. So H unifies with 42 and T unifies with the empty list []. The tail of a one-element list is always [].
2. What does [_, X | _] = [a, b, c, d] bind X to?
The first _ matches a, X matches b (the second element), and the trailing _ matches the rest [c, d]. Each _ is independent and can match anything.
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:
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
- Base case: the last element of a one-element list
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
- Base case: appending L to
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_appendpredicate!
- Base case: reverse of
% --- 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).
Solution
% --- Worked example (provided) ---
% my_member(X, List) - X is an element of List
my_member(X, [X|_]).
my_member(X, [_|T]) :- my_member(X, T).
% --- Solutions ---
% my_last(X, List) - X is the last element of List
my_last(X, [X]).
my_last(X, [_|T]) :- my_last(X, T).
% my_append(L1, L2, L3) - L3 is L1 concatenated with L2
my_append([], L, L).
my_append([H|T1], L2, [H|T3]) :- my_append(T1, L2, T3).
% my_reverse(List, Reversed) - reverse a list
my_reverse([], []).
my_reverse([H|T], R) :- my_reverse(T, RT), my_append(RT, [H], R).
Why this works:
my_last: The base casemy_last(X, [X])says the last element of a one-element list is that element. The recursive case skips the head and recurses on the tail. Each recursive call makes the list one element shorter until only one remains.my_append: The base casemy_append([], L, L)says appending anything to[]gives that thing. The recursive case moves the headHfrom L1 to the result, and recurses on the tail. Trymy_append(X, Y, [1,2,3])— it finds ALL ways to split the list! This is the power of relational programming.my_reverse: Reverses the tail first, then appends[H]at the end. This is an O(n^2) algorithm (eachmy_appendis O(n)), but it clearly shows the recursive structure. A more efficient version uses an accumulator.
Step 7 — Knowledge Check
Min. score: 80%
1. How does my_member(b, [a, b, c]) succeed?
Prolog tries the first clause: does b unify with the head a? No. So it tries the second clause, which recurses on the tail [b, c]. Now the first clause tries: does b unify with b? Yes! The query succeeds.
2. What does my_append(X, Y, [1, 2, 3]) do?
Because my_append is a relation (not a function), Prolog can find ALL combinations: X=[], Y=[1,2,3]; X=[1], Y=[2,3]; X=[1,2], Y=[3]; X=[1,2,3], Y=[]. This is the power of multi-directional predicates!
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:
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
- Base: sum of
length_list(List, Len)— compute the length of a list- (Same pattern as
my_lengthfrom Step 6)
- (Same pattern as
average(Name, Avg)— a student’s average score- Look up the student’s scores, compute sum and length, divide
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?
% 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) :- ...
Solution
% 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
sum_list([], 0).
sum_list([H|T], Sum) :- sum_list(T, Rest), Sum is H + Rest.
% length_list(List, Len) - number of elements in List
length_list([], 0).
length_list([_|T], Len) :- length_list(T, L1), Len is L1 + 1.
% average(Name, Avg) - compute a student's average score
average(Name, Avg) :-
student(Name, Scores),
sum_list(Scores, Sum),
length_list(Scores, Len),
Avg is Sum / Len.
% above_average(Name, Threshold) - true if average > Threshold
above_average(Name, Threshold) :-
average(Name, Avg),
Avg > Threshold.
Why this works:
sum_listandlength_listfollow the standard recursive list pattern: base case for[], recursive case strips the head and recurses on the tail.average/2composes predicates: It looks up the student’s scores withstudent(Name, Scores), computes the sum and length, then divides. This is Prolog’s equivalent of calling helper functions — but notice there are no explicit return values. Each predicate binds its result to a variable.above_average/2computes the average then checks the threshold. When you queryabove_average(Name, 80)with an unboundName, Prolog backtracks through allstudentfacts, computing each average and checking the condition. This is the declarative way to “filter” — no loops, no list comprehensions.- Multi-directional power:
above_average(carol, 90)checks one student.above_average(Name, 80)finds all students above 80. Same predicate, different query directions.
Step 8 — Knowledge Check
Min. score: 80%1. To find all students with average above 80, an imperative programmer would write a loop. In Prolog, you:
You define the relationship (what it means to be above average), then query with a variable for Name. Prolog’s backtracking finds all students that satisfy the condition. No explicit iteration needed.
2. What makes above_average(Name, 80) work for finding ALL qualifying students?
When average(Name, Avg) tries to satisfy itself, it looks up student(Name, Scores). Prolog tries each student fact through backtracking. For each one where Avg > 80, it reports a solution. This is the declarative way to “search” — you describe what you want, and backtracking does the searching.