Week 1: Notes

Prolog

Prolog is a logic programming language. In fact the name Prolog comes from the phrase "programming in logic". In the logic programming paradigm, a program consists of a set of logical statements. In fact Prolog is actually a theorem prover: given a statement, it attempts to prove that it can be true. Prolog's syntax and semantics are closely related to first-order logic, which you may have studied in another class.

As I mentioned in the lecture, in our class we will use SWI-Prolog, which you should install on your own machine now. To start the Prolog REPL, run the command "swipl":

$ swipl
Welcome to SWI-Prolog (threaded, 64 bits, version 8.2.4)
SWI-Prolog comes with ABSOLUTELY NO WARRANTY. This is free software.
Please run ?- license. for legal details.

For online help and background, visit https://www.swi-prolog.org
For built-in help, use ?- help(Topic). or ?- apropos(Word).

1 ?- 

terms

Data in Prolog is represented as terms. For now, a term may be

We will learn about other kinds of terms soon.

equality

The built-in predicate '=' is true when two terms are equal.

?- red = red.
true.

?- red = blue.
false.

dif(X, Y) is true when X and Y are different, i.e. not equal.

?- dif(red, blue).
true.

?- dif(red, red).
false.

Boolean operators

Prolog includes these Boolean operators:

?- red = blue, blue = blue.
false.

?- red = blue; blue = blue.
true.

queries with variables

A Prolog query may include variables. Prolog attempts to prove that the query is true for some values of the given variables. In other words, the variables are existentially quantified. If the query can be true, Prolog reports the values that make it so.

?- X = red.
X = red.

?- X = red, X = blue.
false.

?- X = red, (Y = green; Y = blue).
X = red,
Y = green ;
X = red,
Y = blue.

A query may succeed multiple times, as in the last query above. When there may be multiple solutions, Prolog prints the first solution, then waits for user input. At this point you have two choices:

Here are some more queries involving multiple variables:

?- (X = blue; X = red), (Y = red; Y = green), X = Y.
X = Y, Y = red

?- (X = blue; X = red), (Y = red; Y = green), dif(X, Y).
X = blue,
Y = red ;
X = blue,
Y = green ;
X = red,
Y = green.

predicates

Unlike most programming languages, Prolog has no built-in concept of functions. Instead, it has predicates, also known as relations.

A predicate in Prolog has a name and zero or more arguments. A predicate's name begins with a lowercase letter. Predicates are defined in a source file using a series of clauses.

facts

One kind of clause is a fact. A fact contains a predicate name plus zero or more arguments, which are terms.

For example, here is a series of facts that define several predicates named 'green', 'blue', 'animal' and 'frog'. All of these predicates take a single argument. In this code, the arguments 'grass', 'frog' and so on are atoms.

green(grass).
green(frog).

blue(sky).
blue(water).

animal(frog).
animal(dog).
animal(cat).

plant(tree).
plant(grass).

Let's put these facts into a source file 'nature.pl', then load it into the 'swipl' interpreter:

$ swipl nature.pl

?- green(grass).          % is grass green?
true.                     % yes

?- green(X).              % is some X green?
X = grass ;               % X could be grass...
X = frog.                 % or a frog

?- green(X), animal(X).   % is there some green animal X?
X = frog.                 % yes, X is a frog

Here are several facts that define a predicate 'capital' that takes two arguments:

capital(prague, czech_rep).
capital(berlin, germany).
capital(bratislava, slovakia).

Let's make some queries involving this predicate:

?- capital(prague, czech_rep).   % is Prague the capital of the Czech Republic?
true.                            % yes

?- capital(X, czech_rep).        % what is the capital of the Czech Republic?
X = prague.                      % it is Prague

?- capital(prague, X).           % what is Prague the capital of?
X = czech_rep.                   % the Czech Republic

We see that we may run a predicate in any direction. In other words, we may specify values for one (or more arguments), and ask Prolog to find values for the other arguments that cause the predicate to be true. In this way Prolog predicates are quite different from functions in procedural languages, which can run in only one direction.

rules

Another kind of clause is a rule, which consists of a head and a body, separated by the symbol ':-' . The symbol ':-' means 'if', i.e. logical implication. Specifically,

A :- B

means "A is true if B is true", or "B implies A". In this statement, A is the head and B is the body.

For example, let's return to the facts about nature that we wrote above. Let's define a new predicate 'alive' using two rules:

alive(X) :- animal(X).
alive(X) :- plant(X).

The first rule says "If X is an animal, then X is alive". Or, equivalently, "all animals are alive". Similarly, the second rule says "all plants are alive".

Logically speaking, the variable X in these rules is universally quantified. In other words, the rule is a statement about all X.

Now we can make queries using this predicate:

?- alive(X).            % what is alive?
X = frog ;
X = dog ;
X = cat ;
X = tree ;
X = grass.

?- alive(X), green(X).  % what is alive and green?
X = frog ;
X = grass.

a family tree

Here is a family tree of part of the Czech royal family from hundreds of years ago:

In this picture, "Charles" is King Charles IV. Men are blue, and women are pink. The notation A → B means "A is the parent of B". For example, Charles's parents were John and Elizabeth B.

Let's express this tree as a series of Prolog facts:

male(john).
male(charles).
male(wenceslaus).
male(sigismund).

female(elizabeth_b).
female(anne).
female(elizabeth_p).
female(margaret).
female(barbara). 
female(elizabeth_l).

parent(john, charles).
parent(elizabeth_b, charles).
parent(anne, wenceslaus).
parent(charles, wenceslaus).
parent(charles, margaret).
parent(elizabeth_p, margaret).
parent(charles, sigismund).
parent(elizabeth_p, sigismund).
parent(sigismund, elizabeth_l).
parent(barbara, elizabeth_l).

We may now write a query asking who was Charles's mother:

?- parent(X, charles), female(X).   % who is Charles's parent and is female?
X = elizabeth_b.

To generalize this idea, let's write a predicate mother(M, P) that is true if M is the mother of P:

mother(M, P) :- parent(M, P), female(M).

Now we can write the above query more simply:

?- mother(M, charles).
M = elizabeth_b.

Here are two more predicates expressing family relations:

% C is a child of P.
child(C, P) :- parent(P, C).

% G is a grandparent of P.
grandparent(G, P) :- parent(G, X), parent(X, P).

We may interpret the 'grandparent' predicate as follows. For any G and P, G is the grandfather of P if there is some X such that G is the parent of X, and also X is the parent of P. In other words, the variables G and P are universally quantified. The variable X, which appears only in the body of the rule, is existentially quantified.

a recursive predicate

Let's write a predicate ancestor(P, Q) which is true if P is an ancestor of Q, i.e. a parent, grandparent, great-grandparent, …

ancestor(P, Q) :- parent(P, Q).                  % base case

ancestor(P, Q) :- parent(P, X), ancestor(X, Q).  % recursive case

This predicate is recursive, because in the second rule 'ancestor()' appears in the body, i.e. the predicate calls itself.

The first rule above says "If P is a parent of Q, then P is an ancestor of Q". The second says "If P is a parent of some X, and X is an ancestor of Q, then P is an ancestor of Q".

It works:

?- ancestor(A, margaret).        % who are Margaret's ancestors?
A = charles ;
A = elizabeth_p ;
A = john ;
A = elizabeth_b ;
false.

?- ancestor(elizabeth_p, P).     % who are Elizabeth P's descendents?
P = margaret ;
P = sigismund ;
P = elizabeth_l ;
false.

Now suppose that we swap the order of the goals in the recursive case, so that it looks like this:

ancestor(P, Q) :- ancestor(X, Q), parent(P, X).  % recursive case

This has no effect on the logical meaning of the rule. However, now the queries above will produce some results, then fail to terminate:

?- ancestor(elizabeth_p, P).
P = margaret ;
P = sigismund ;
P = elizabeth_l ;
ERROR: Stack limit (1.0Gb) exceeded

We see that reordering goals may affect whether Prolog's search strategy will succeed. In general, I recommend that you place the recursive call last when writing a recursive predicate, which will help with termination.