← AS/A Level

CS · Chapter 29

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
Chapter 29 opener: declarative programming

🎯 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.

Imperative: how
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.

Declarative: what
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).

Figure 29.01The SWI-Prolog environment, waiting at the ?- prompt

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.
Full stop! Every clause, and every query, ends with a full stop (.). 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 termRuleExamples
AtomA general-purpose name with no built-in meaning. Always starts with a lower-case letter.paris, fred, aubergine
NumberAn integer or a float (real).250, 3.14
VariableStarts 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)
Arity: the number of arguments a predicate takes. capitalCity(paris) has arity 1, written capitalCity/1.
Case matters. Prolog is case sensitive. 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. Both capitalCity and paris are atoms.
  • capitalCity is the predicate and paris is its argument.
  • It has one argument, so it is capitalCity/1.

✏️ Task 29.01

Task 29.01 In SWI-Prolog, open the editor (File, New …), type the three clauses as in Figure 29.02, and save the file (File, Save buffer) as Ex1.
Figure 29.02Three capitalCity facts typed into the SWI-Prolog editor and saved as Ex1.pl

Asking questions

Clauses 01 to 03 are a knowledge base, so we can query it.

Is Paris a capital city?
capitalCity(paris).
Prolog answers true. Yes, it is.
Is London a capital city?
capitalCity(london).
Prolog answers false. The fact is not in our knowledge base.
Closed world: false does not mean London is not a capital. It means Prolog cannot prove it from what it has been told.

✏️ Task 29.02

Task 29.02 Run your own queries. First consult the knowledge base (File, Consult …) from the Prolog environment. SWI-Prolog uses the prompt ?- (Figure 29.03).
Figure 29.03Example queries in SWI-Prolog: capitalCity(paris) is true, capitalCity(london) is false

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).
Which country is Berlin in?
cityInCountry(berlin, Country).
Country starts with a capital letter, so it is a variable.
Which cities are in Germany?
cityInCountry(City, germany).
Figure 29.04Instantiations of a variable: Country = germany; City = berlin ; City = munich

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).
Figure 29.05Instantiation of variables: the two ingredients of a tagine and their amounts

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).
Book wording: the book says “we modify the query”, but it is the rule that changes. The query sibling(jack, X). stays the same.

❓ Question 29.01

Question 29.01 What answer do you expect from Prolog for the query 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

Task 29.03 Write a knowledge base for your own family. You can add more predicates, for example:
PredicateMeaning
male(fred).Fred is male
female(alia).Alia is female
Write a rule for father. Test your program.
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.

Instantiation: the = 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).

Figure 29.06Switching on the trace facility with trace. before the goal containsMeat(X)

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.

Figure 29.07The graphical debugger: the knowledge base, the bindings and the call stack

Without the graphical debugger, trace. shows the trace in the SWI-Prolog window itself (Figure 29.08).

Figure 29.08SWI-Prolog trace of the goal containsMeat(X): Call, Exit, Redo and Fail ports

Trace terminology

PortMeaning
CallThe first entry to a predicate
creepProlog moves on to the next predicate (what you choose each time you press the space bar)
ExitA successful return: the goal has been proved
RedoProlog backs into the predicate to look for another answer
FailProlog can find no (more) solutions for the goal

Reading Figure 29.08

  1. ingredient(X, Meat, _) exits with the first fact: tagine, aubergine, 250.
  2. meat(aubergine) fails, so Prolog redoes ingredient and gets tagine, tomato, 100.
  3. meat(tomato) fails, so it redoes ingredient again: stew, beef, 400.
  4. meat(beef) exits, so containsMeat(stew) exits and Prolog prints X = stew.
  5. We type ;. Prolog redoes ingredient: stew, potato, 600. meat(potato) fails, there are no more ingredient facts, so containsMeat fails: 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.
Figure 29.09Recursive rules: the base case and the general case of ancestor/2
ancestor(A, B) :- parent(A, B).                  /* the base case */
ancestor(A, B) :- parent(A, X), ancestor(X, B).  /* the general case */
A recursive rule must:
  • Have a base case
  • Have a general case
  • reach the base case after a finite number of calls to itself
These are the same rules recursion must follow in imperative programming.

✏️ Task 29.04

Task 29.04 Add the ancestor rules to the family knowledge base and check that this query gives the correct results: 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

Task 29.05 Enter the code from Worked Example 29.05 into the Prolog editor, save it and consult it. Then ask 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.

Head and tail: any non-empty list has two parts. The head is the first item. The tail is the list that is left when the first item is taken away, so the tail is always a list.

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 definitionQueryProlog 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:

QueryProduces
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.

QueryProduces
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: '). outputs message:, and write(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 instantiates Name to it. The input must start with a lower-case letter and must not contain spaces or be in quotes.
  • nl moves 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/1 adds the clause given as its argument to the knowledge base.
  • retractall/1 takes 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.

Watch out: if the user types anything other than the six expected words (say 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

Task 29.06 Test the recursively defined rule 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.

Book typo: the task text says 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.
Reflection point: can you explain the difference between a fact and a rule?

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

1 A logic programming language represents details of cities of the world as facts and rules, in clauses 1 to 17:
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).
ClauseMeaning
01Vienna is a capital.
06Vienna is in Austria.
10Austria is in the continent of Europe.
14I visited the city of Vienna.
16City is the capital of Country if City is a capital and it is in Country.
17City 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 City linking 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

2 In one country, to become a qualified driver you must:
  • 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)
A declarative programming language represents this knowledge base:
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).
ClauseMeaning
01The minimum age for a car licence is 18.
03Fred is aged 19.
09Fred has a licence.
15Person 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, _)).

  1. Clause 15 matches the goal, with X = mike and V = car. Its body is tried left to right.
  2. hasLicence(mike): clauses 09 and 10 do not match. Clause 11 does, so it returns true.
  3. minimumAge(car, L): clause 01 matches, so L = 18.
  4. age(mike, A): clauses 03 and 04 do not match. Clause 05 does, so A = 17.
  5. A >= L: 17 >= 18 is false. Prolog backtracks. No other age(mike, …) fact exists, clause 02 is for trucks so minimumAge(car, L) has no other match, and there is no other hasLicence(mike). Clause 15 fails, so the output is false.
Book typo: the question prints the goal as 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)