Cambridge AS & A Level · Part 4 · Chapter 29
Declarative Programming
Instead of telling the computer how to solve a problem step by step, you describe what is true and ask questions. This chapter is about Prolog: facts, rules, queries, backtracking, recursive rules and lists. Every task, worked example and question from the chapter is here, and a working Prolog console sits at the bottom of every tab.
Guru Jazzy🎯 Learning objectives
By the end of this chapter you should be able to:
- understand a problem and solve it by writing suitable facts and rules from the information you are given
- understand and write code that satisfies a goal using facts and rules
🧭 How versus what
Every program you have written so far has been imperative: a list of steps in order. A declarative program states the facts and rules of the problem instead, then asks a question. The language works out the steps for itself.
FOR Index ← 1 TO NumCities
IF Country[Index] = "germany"
THEN
OUTPUT City[Index]
ENDIF
NEXT Index You choose the loop, the test and the output.
cityInCountry(berlin, germany). cityInCountry(munich, germany). cityInCountry(paris, france). ?- cityInCountry(City, germany).
You state facts and ask. Prolog does the searching.
🗂️ 29.01 Declarative programming languages
Declarative languages include:
- database query languages such as SQL (Chapter 11, Section 11.07)
- Regular expressions
- Logic programming, for example Prolog
- Functional programming
Prolog is a logic programming language. It is widely used for artificial intelligence and expert systems. The book's programs were written in the free SWI-Prolog environment (swi-prolog.org).
You do not need to install anything to follow this page: the Prolog console at the bottom of every tab runs the book's programs in your browser.
🧱 29.02 Prolog basics
Prolog has three basic constructs: facts, rules and queries.
- The program logic is written as clauses (facts and rules).
- A problem is solved by running a query, also called a goal.
- A collection of clauses is a knowledge base. Writing a Prolog program means writing a knowledge base. Using it means asking queries.
A clause has the form:
Head :- Body.
.). Forgetting it is the most common Prolog mistake.Terms: Prolog's one data type
Everything in Prolog is a term. A term can be:
| Kind of term | Rule | Examples |
|---|---|---|
| Atom | A general-purpose name with no built-in meaning. Always starts with a lower-case letter. | paris, fred, aubergine |
| Number | An integer or a float (real). | 250, 3.14 |
| Variable | Starts with a capital letter or an underscore (_). | Country, City, _ |
| Compound term (predicate) | An atom followed by arguments in brackets. The arguments can themselves be compound terms. | capitalCity(paris) |
capitalCity(paris) has arity 1, written capitalCity/1.paris is an atom, Paris is a variable, and capitalcity is a different predicate from capitalCity.🎮 What kind of term is it?
Decide from the first character and the brackets.
📚 29.03 Facts in Prolog
A clause without a body is a fact:
01 capitalCity(paris). 02 capitalCity(berlin). 03 capitalCity(cairo).
- Clause 01 means: Paris is a capital city.
capitalCity(paris)is a compound term. BothcapitalCityandparisare atoms.capitalCityis the predicate andparisis its argument.- It has one argument, so it is
capitalCity/1.
✏️ Task 29.01
Asking questions
Clauses 01 to 03 are a knowledge base, so we can query it.
capitalCity(paris).Prolog answers
true. Yes, it is.capitalCity(london).Prolog answers
false. The fact is not in our knowledge base.false does not mean London is not a capital. It means Prolog cannot prove it from what it has been told.✏️ Task 29.02
?- (Figure 29.03).If a query gets no response, check that you have:
- consulted the knowledge base (the green text in Figure 29.03)
- Used lower-case letters where you should
- Ended the query with a full stop
The console below behaves the same way: leave off the full stop and it tells you Prolog is still waiting for one.
🔤 29.04 Prolog variables
Add more facts. Comments go between /* and */.
04 cityInCountry(paris, france). /* Paris is a city in France */ 05 cityInCountry(berlin, germany). 06 cityInCountry(cairo, egypt). 07 cityInCountry(munich, germany).
cityInCountry(berlin, Country).Country starts with a capital letter, so it is a variable.cityInCountry(City, germany).When a query with a variable has more than one answer, type a semicolon after the first answer and Prolog gives the next one. The semicolon means OR. First City is instantiated to berlin, and then to munich.
🍲 Worked Example 29.01 Using a knowledge base
01 vegetable(aubergine). /* aubergine is a vegetable */ 02 vegetable(potato). 03 vegetable(tomato). 04 meat(chicken). /* chicken is a type of meat */ 05 meat(beef). 06 meat(lamb). 07 ingredient(tagine, aubergine, 250). /* tagine contains 250g aubergine */ 08 ingredient(tagine, tomato, 100). 09 ingredient(stew, beef, 400). 10 ingredient(stew, potato, 600).
To check the ingredients of a tagine, ask:
ingredient(tagine, Ingredient, Amount).
Two variables are instantiated together for each answer: first aubergine and 250, then tomato and 100.
➖ 29.05 The anonymous variable
If we do not care about the amount, we use the anonymous variable, the underscore _:
ingredient(tagine, Ingredient, _).
The _ still has to match something, but Prolog does not report what it matched. Each _ is a separate variable, even when there are two in one clause.
🧩 29.06 Rules in Prolog
Remember a clause has the form Head :- Body. A rule has a body. The body is made of calls to predicates, which are the rule's goals. Each predicate is either true or false depending on its terms. If the body is true, the head is true too.
:- means IF, (comma) means AND👨👦 Worked Example 29.02 Using rules in a knowledge base
01 parent(fred, jack). /* Fred is the father of Jack */ 02 parent(fred, alia). 03 parent(fred, paul). 04 parent(dave, fred).
G is a grandparent of S if G is a parent of P and P is a parent of S. In words:
grandparent(G, S) IF parent(G, P) AND parent(P, S).
In Prolog, IF becomes :- and AND becomes a comma:
grandparent(G, S) :- parent(G, P), parent(P, S).
Siblings
Two people are siblings if they have the same parent:
sibling(A, B) :- parent(P, A), parent(P, B).
The query sibling(jack, X). gives the answers we expect, but it also says that Jack is his own sibling, because B can be matched with jack again. To stop this we add a goal saying A must not equal B:
sibling(A, B) :- parent(P, A), parent(P, B), not(A=B).
sibling(jack, X). stays the same.❓ Question 29.01
sibling(dave, X).?false. To be a sibling, Dave needs a parent: the first goal is parent(P, dave). No fact has dave as the child, so that goal fails immediately and the rule can never succeed. Dave is a parent in this knowledge base, never a child.
✏️ Task 29.03
| Predicate | Meaning |
|---|---|
| male(fred). | Fred is male |
| female(alia). | Alia is female |
male(fred). male(jack). male(paul). male(dave). female(alia). father(F, C) :- parent(F, C), male(F). mother(M, C) :- parent(M, C), female(M). son(S, P) :- parent(P, S), male(S). daughter(D, P) :- parent(P, D), female(D).
Test it with father(F, alia). (F = fred) and daughter(D, fred). (D = alia). Try mother(M, jack). as well: the answer is false, because no mother is in the knowledge base. Prolog only knows what it has been told.
🥩 Worked Example 29.03 Adding a rule to a knowledge base
Using the knowledge base from Worked Example 29.01, we want to know which dishes contain meat. We don't care how much, so the third argument of ingredient/3 can be the anonymous variable:
containsMeat(X) :- ingredient(X, Meat, _), meat(Meat).
The query containsMeat(X). returns X = stew.
Notice that Meat links the two goals. Whatever the first goal instantiates Meat to, the second goal checks it.
🎮 Fact, rule or query?
Look for :- and the ?- prompt.
🔙 29.07 Instantiation and backtracking
Prolog answers a query such as the one in Worked Example 29.03 with X = stew.
= sign here is not an assignment as in an imperative program. It shows that the variable has been instantiated (matched) to a value.How does Prolog use the knowledge base to reach its answers? It tries the clauses from top to bottom and the goals in a body from left to right. When a goal fails, it goes back to the most recent goal that had another option and tries that option instead. This is backtracking. The best way to watch it is the debugger.
🐞 Worked Example 29.04 Tracing a goal
Consult the knowledge base from Worked Example 29.03, start the graphical debugger (Debug, Graphical debugger), type trace. and then the goal (Figure 29.06).
The graphical debugger window opens (Figure 29.07). Press the space bar to step through the program. When Prolog gives an answer in the main window, type a semicolon so it goes on to look for another possible answer.
Without the graphical debugger, trace. shows the trace in the SWI-Prolog window itself (Figure 29.08).
Trace terminology
| Port | Meaning |
|---|---|
| Call | The first entry to a predicate |
| creep | Prolog moves on to the next predicate (what you choose each time you press the space bar) |
| Exit | A successful return: the goal has been proved |
| Redo | Prolog backs into the predicate to look for another answer |
| Fail | Prolog can find no (more) solutions for the goal |
Reading Figure 29.08
ingredient(X, Meat, _)exits with the first fact: tagine, aubergine, 250.meat(aubergine)fails, so Prolog redoesingredientand gets tagine, tomato, 100.meat(tomato)fails, so it redoesingredientagain: stew, beef, 400.meat(beef)exits, socontainsMeat(stew)exits and Prolog printsX = stew.- We type
;. Prolog redoesingredient: stew, potato, 600.meat(potato)fails, there are no more ingredient facts, socontainsMeatfails:false.
🐞 Step through a trace yourself
A cut-down graphical debugger. Each step is one port. The line being used in the knowledge base lights up: amber while a goal is being called, green when a clause proves it.
🎮 Which port?
🔁 29.08 Recursion
Chapter 24 covered recursion in imperative languages. In a declarative language, recursion is where a rule is defined by itself: more precisely, a rule uses itself as a sub-goal.
Let us extend the family knowledge base from Worked Example 29.02. When is A an ancestor of B?
- If A is a parent of B, A is an ancestor of B.
- If A is a parent of P, and P is a parent of B, A is an ancestor of B.
- The same holds for a parent of a parent of a parent of B, and so on.
- In general: if A is a parent of X, and X is an ancestor of B, then A is an ancestor of B.
ancestor(A, B) :- parent(A, B). /* the base case */ ancestor(A, B) :- parent(A, X), ancestor(X, B). /* the general case */
- Have a base case
- Have a general case
- reach the base case after a finite number of calls to itself
✏️ Task 29.04
ancestor(A, jack).A = fred ; A = dave ; false.
The base case finds Fred (parent(fred, jack)). The general case then tries each parent fact in turn. It succeeds through parent(dave, fred), because Fred is an ancestor of Jack, so Dave is too. No other route works, so the last answer is false.
🧮 Worked Example 29.05 The factorial function in Prolog
Chapter 24, Worked Example 24.01 programmed factorial with recursion in an imperative language. It can also be written recursively in Prolog:
factorial(0, 1). /* base case: 0! = 1 */ factorial(N, Result) :- /* Result = N! */ M is N - 1, /* assign N-1 to M */ factorial(M, PartResult), /* PartResult = (N-1)! */ Result is PartResult * N. /* Result = N * (N-1)! */
is evaluates the arithmetic on its right and matches the result with the left. M = N - 1 would not work, because = only matches terms: M would become the term 5-1, not 4.
✏️ Task 29.05
factorial(5, Answer). Do you get the correct answer?Yes: Answer = 120.
Now type ; to ask for another answer. Prolog backtracks into the second clause with N = 0, then tries −1, −2, … and never stops, until it runs out of stack. The base case factorial(0, 1) was found, but nothing stops the general case being used for 0 as well. The fix is a guard, so the general case only applies when N > 0:
factorial(0, 1). factorial(N, Result) :- N > 0, M is N - 1, factorial(M, PartResult), Result is PartResult * N.
📋 29.09 Lists
A list is an ordered collection of terms, written in square brackets with commas between the terms. [] is the empty list. For example [1, 2, 3] or [red, green, blue]. An element can be any Prolog object, and different types can be mixed in one list. Prolog uses lists where a procedural language might use arrays.
A vertical bar | separates the head from the tail. If Prolog matches [H|T] to [car, lorry, boat, ship], it instantiates H to car and T to [lorry, boat, ship].
| Clause definition | Query | Prolog responds |
|---|---|---|
| showHeadAndTail([H|T], H, T). | showHeadAndTail([fred, jack, emma], Head, Tail). | Head = fred, Tail = [jack, emma]. |
| myList([1,2,3]). | myList([H|T]). | H = 1, T = [2, 3]. |
| emptyList(A) :- A = []. | emptyList([1]). | false. |
🎮 Head-and-tail splitter
A fresh list every time. What does each variable become?
List-processing predicate: append
The built-in predicate append(A, B, C) joins list A and list B to give list C. It also works backwards:
| Query | Produces |
|---|---|
| append([a, b], [c, d], MyList). | MyList = [a, b, c, d]. |
| append(FirstList, [c, d], [a, b, c, d]). | FirstList = [a, b]. |
List-processing predicate: member
The built-in predicate member(A, B) is true if item A is in list B.
| Query | Produces |
|---|---|
| member(c, [a, b, c, d, e]). | true. |
| member(X, [a, b, c, d]). | X = a ; X = b ; X = c ; X = d. |
List-processing predicates: write and read
write(A)outputs A to the screen.write('message: ').outputsmessage:, andwrite(X).outputs the value X is currently instantiated to.read(A)reads a value from the keyboard into variable A.read(Name).waits for an atom to be typed and instantiatesNameto it. The input must start with a lower-case letter and must not contain spaces or be in quotes.nlmoves the output to a new line.
With read and write we can write user-friendly programs.
🌦️ Worked Example 29.06 Using read and write
The conversation with the user is one rule, go, whose steps are separated by commas (AND).
assert/1adds the clause given as its argument to the knowledge base.retractall/1takes matching clauses out again, so that next time the program runs the new facts are added and used in the goal.
/* Weather knowledge base */
weather(good) :-
temp(high),
humidity(dry),
sky(sunny).
weather(bad) :-
(humidity(wet);
temp(low);
sky(cloudy)).
/* interface */
go :-
write('Is the temperature high or low? '),
read(Temp), nl,
write('Is the sky sunny or cloudy? '),
read(Sky), nl,
write('Is the humidity dry or wet? '),
read(Humidity), nl,
assert(temp(Temp)),
assert(sky(Sky)),
assert(humidity(Humidity)),
weather(Weather),
write('The weather is '), write(Weather),
retractall(temp(_)),
retractall(sky(_)),
retractall(humidity(_)). To run the program, type go. Inside weather(bad) the semicolons mean OR: any one of the three conditions makes the weather bad.
In the console, type your three answers into the Input for read/1 box first, one per line (for example high, sunny, dry), then run go.
warm), weather(Weather) fails, so go fails before the retractall lines run. The asserted facts stay in the knowledge base and change the next run's answer. Try it: answer warm, sunny, dry (the answer is false), then change the inputs to high, cloudy, dry and run go. again without re-consulting. It says the weather is good, even though the sky is cloudy: the leftover sky(sunny) fact still lets weather(good) succeed. One fix is to run the three retractall goals at the start of go as well.✏️ Task 29.06
writeList/1, which outputs the elements of a list:
writeList([]). writeList([H|T]) :- write(H), nl, writeList(T).
writeList([red, green, blue]). prints each element on its own line, then answers true. The second clause writes the head, then calls itself on the tail. The list gets shorter on every call until the base case writeList([]) matches the empty list.
writelist/1 (lower-case l) but the code defines writeList. Prolog is case sensitive, so writelist([a,b]). gives an “unknown procedure” error. Try both in the console.A fact is a clause with no body. It is unconditionally true: meat(beef). A rule has a head and a body joined by :-. The head is true only if every goal in the body can be proved: containsMeat(X) :- ingredient(X, Meat, _), meat(Meat). Facts store knowledge. Rules let Prolog work out new knowledge from it.
📌 Summary
- Imperative programs set out the steps for how to solve a problem.
- Declarative programs describe what the problem is.
- A knowledge base is made of two types of clause: facts and rules.
- Clauses are sometimes called predicates.
- The arity of a predicate is the number of arguments it takes.
- To solve a problem, the user of the knowledge base poses a query.
- A recursive rule is defined in terms of itself.
- In logic programming, a list is handled by separating the head from the tail:
[H|T].
🃏 Key-term flashcards
Tap a card to flip it.
✅ Quick quiz
📝 Exam-style questions
Write your answer first, then open the model answer. These are my mark-scheme-style points, not official mark schemes. The ▶ buttons load each knowledge base into the console, written in real Prolog (IF becomes :-, AND becomes a comma), so you can test your own clauses.
Question 1: cities of the world
01 capital(vienna).
02 capital(london).
03 capital(santiago).
04 capital(caracas).
05 capital(tokyo).
06 cityIn(vienna, austria).
07 cityIn(santiago, chile).
08 cityIn(salzburg, austria).
09 cityIn(maracaibo, venezuela).
10 continent(austria, europe).
11 continent(chile, southAmerica).
12 continent(uk, europe).
13 continent(argentina, southAmerica).
14 iVisited(vienna).
15 iVisited(tokyo).
16 capitalOf(City, Country)
IF capital(City) AND cityIn(City, Country).
17 europeanCity(City)
IF cityIn(City, Country) AND continent(Country, europe). | Clause | Meaning |
|---|---|
| 01 | Vienna is a capital. |
| 06 | Vienna is in Austria. |
| 10 | Austria is in the continent of Europe. |
| 14 | I visited the city of Vienna. |
| 16 | City is the capital of Country if City is a capital and it is in Country. |
| 17 | City is a city in Europe if City is in Country and Country is in Europe. |
(i) cityIn(london, uk).
(ii) iVisited(strasbourg).
Lower-case atoms, the same predicate names and argument order as the existing clauses, and a full stop.
chile, argentina: one mark for each, in that order (clauses 11 and 13).
countriesIVisited(Country) IF iVisited(City) AND cityIn(City, Country).
iVisited(City)[1]cityIn(City, Country)[1]- joined by AND, with the same variable
Citylinking the two goals [1]
With this knowledge base the only result is austria. Tokyo was visited, but there is no cityIn(tokyo, …) fact, so Prolog cannot prove which country it is in.
Question 2: qualified drivers
- have a licence: there is a minimum age for one, and it is different for cars and trucks
- pass a theory test: the same test for cars and trucks
- pass a driving test for a specific vehicle (car or truck)
01 minimumAge(car, 18).
02 minimumAge(truck, 21).
03 age(fred, 19).
04 age(jack, 22).
05 age(mike, 17).
06 age(jhon, 20).
07 age(emma, 22).
08 age(sheena, 19).
09 hasLicence(fred).
10 hasLicence(jack).
11 hasLicence(mike).
12 hasLicence(jhon).
13 hasLicence(emma).
14 hasLicence(sheena).
15 allowedToDrive(X, V)
IF hasLicence(X) AND minimumAge(V, L)
AND age(X, A)
AND A >= L.
16 passedTheoryTest(jack).
17 passedTheoryTest(emma).
18 passedTheoryTest(jhon).
19 passedTheoryTest(fred).
20 passedDrivingTest(jhon, car).
21 passedDrivingTest(fred, car).
22 passedDrivingTest(jack, car).
23 passedDrivingTest(jack, truck).
24 passedDrivingTest(sheena, car).
25 qualifiedDriver(X, V)
IF allowedToDrive(X, V)
AND passedTheoryTest(X)
AND passedDrivingTest(X, V). | Clause | Meaning |
|---|---|
| 01 | The minimum age for a car licence is 18. |
| 03 | Fred is aged 19. |
| 09 | Fred has a licence. |
| 15 | Person X is able to drive vehicle V if X has a licence and the age A of X is greater than or equal to the minimum age L to drive V. |
(i) Any of clauses 01–14 or 16–24, for example age(fred, 19).
(ii) Clause 15 (allowedToDrive) or clause 25 (qualifiedDriver).
(i) jack (clause 23 is the only truck test).
(ii) false: Mike has a licence, but he is 17 and the minimum age for a car is 18.
(iii) false: clause 14 says Sheena has a licence, so NOT of it is false.
(i) qualifiedDriver(X, car). The variable gets 1 mark and car as the second argument gets 1. Output: fred, jack, jhon.
(ii)
passedTheoryTest(X) AND NOT(passedDrivingTest(X, V)).
passedTheoryTest(X)[1]NOT( … )around the driving test [1]passedDrivingTest(X, V)with an unbound vehicle (or_), so it means “no driving test at all” [1]
Output: emma. In Prolog it is passedTheoryTest(X), not(passedDrivingTest(X, _)).
- Clause 15 matches the goal, with X = mike and V = car. Its body is tried left to right.
hasLicence(mike): clauses 09 and 10 do not match. Clause 11 does, so it returns true.minimumAge(car, L): clause 01 matches, so L = 18.age(mike, A): clauses 03 and 04 do not match. Clause 05 does, so A = 17.A >= L: 17 >= 18 is false. Prolog backtracks. No otherage(mike, …)fact exists, clause 02 is for trucks sominimumAge(car, L)has no other match, and there is no otherhasLicence(mike). Clause 15 fails, so the output is false.
AllowedToDrive(mike, car). A capital letter would make it a variable in Prolog. It should be allowedToDrive.🦉 Prolog console
A small Prolog that runs in your browser. Pick a knowledge base (or type your own), press Consult, then ask a query after ?-. Use ; next for another answer and . stop to finish, just like typing ; or . in SWI-Prolog. Tick trace to see Call, Exit, Redo and Fail.
Supports facts, rules, queries, , ; not/1, =, is and comparisons, lists with [H|T], append, member, length, write, nl, read, assert, retract, retractall and trace. It is a teaching subset, not full SWI-Prolog (no cut, no modules).
Guru Jazzy · Cambridge AS & A Level Computer Science · Chapter 29 Declarative Programming · Figures from the Cambridge coursebook (Langfield & Duddell)