Table of Contents

Introduction to Prolog

The basics

Consider the following code:

student(gigel).
student(ionel).
student(john).
student(mary).
 
male(gigel).
male(ionel).
male(john).
female(mary).
 
lecture(pp).
lecture(aa).
 
studies(gigel,aa).
studies(mary,pp).
studies(mary,aa).
studies(john,pp).

Atoms

gigel, ionel, john, mary, pp, aa are atoms. Atoms are basic Prolog constructs. Prolog is a weakly-typed language, and primitive types such as String or Integer are part of the language. The afore-mentioned elements have type atom (which is different from String).

Atoms are specific to Prolog, and they are inherited from First-Order Logic, the formalism on which Prolog is based.

Relations

studies, male, female and lecture are relations. In First-Order Logic, a relation over sets $ A_1, \ldots, A_n$ , having arity n, is a subset $ R\subseteq A_1 \times \ldots \times A_n$ . In Prolog, we make no distinction between the types ($ A_i$ ) of each element from a relation. Thus, in Prolog, a relation of arity n is a subset $ R\subseteq Prim$ where $ Prim$ is the set of primitive values from the language. Strings, integers and atoms are such values.

The above program is simply a definition of 4 relations. Mathematically, :$ studies=\{(gigel,aa),(mary,pp),(mary,aa), (john,pp)\}$

Queries

After loading the previous program, the interpreter (with prompt ?-) expects a query from the programmer. Formallly, a query is a conjuction of literals, where each literal is of the form: $ R(t_1,\ldots, t_n)$ and $ t_i$ are terms. Instead of pursuing a formal approach, we illustrate examples of queries, for our above program (in what follows, we prepend ?- to Prolog code to remind the reader we are performing a query):

Clauses

As shown so far, programs behave as databases (relations are essentially tables) and via the interpreter, we can use queries to interrogate the database.

However, a Prolog program is far more than just a database. We can also define new relations based on existing ones, without exhaustively enumerating all members. For instance, let us add to our initial program, the relation all male students which share a lecture with a female student:

hasFemaleColleague(M) :- student(M), male(M), studies(M,L), studies(F,L), female(F).

The above clause or rule can be translated to First-Order Logic (FOL) where it can be read easier:

$ student(M) \wedge male(M) \wedge studies(M,L) \wedge studies(F,L) \wedge female(F) \implies hasFemaleColleague(M)$

Notice the direction of the implication, as well as the usage of , (logical conjunction), :- (implication) and .(the end of a clause).

Re-satisfaction

Let us define a new property (1-ary relation) which describes those lectures which have at least two students. The first attempt:

lectureOfTwo(L) :- lecture(L), studies(X,L), studies(Y,L).

is incorrect, as X and Y may be bound to the same student. Another attempt is:

lectureOfTwo(L) :- lecture(L), studies(X,L), studies(Y,L), X \= Y.

however following the interrogation -? lectureOfTwo(L), pp and aa are prompted twice. To understand this, let us write our relation in FOL:

$ lecture(L) \wedge studies(X,L) \wedge studies(Y,L) \wedge X \neq Y \implies lectureofTwo(L)$

Formally, FOL sentences require that all variables are within the scope of a quantifier. Thus, to be more exact, we should write:

$ \forall L( lecture(L) \wedge \exists X \exists Y (studies(X,L) \wedge studies(Y,L) \wedge X \neq Y) \implies lectureofTwo(L))$

Notice the scope of each quantification, as a change in the parentheses may modify the meaning of the sentence.

Prolog does not interpret the sentence as in FOL. Instead, it finds all combinations of values for L, X and Y that satisfy the left-hand side (LHS) of the implication. In our example, these are:

pp, mary, john
pp, john, mary
aa, gigel, mary
aa, mary, gigel

and then reports the values of L for each such combination. For this reason, we observe that both aa and pp are reported twice.

We can verify this by querying: -? lecture(L), studies(X,L), studies(Y,L), X \= Y.

Proof trees

We briefly discuss the concept of proof trees which will be approached in more detail later on, when discussing resolution. To introduce it, consider the queries:

-? female(X), studies(X,L).

and

-? studies(X,L), female(X).

Viewed as FOL sentences (with appropriate quantification), the queries are the same. However, in answering those queries, Prolog searches its knowledge-base (database) differently.

For the first query:

Find X which satisfies female(X):
  X = mary,
  Find L which satisfies studies(mary,L):
  L = pp,
    report X = mary, L = pp.
  L = aa
    report X = mary, L = aa.

And for the second:

Find X,L which satisfy studies(X,L):
  X = gigel, L = aa
    female(gigel) could not be established, continue search
  X = mary, L = pp
    female(mary) is true, report X = mary, L = pp.
  X = mary, L = aa
    female(mary) is true, report X = mary, L = aa.
  X = john, L = pp
    female(john) could not be established, search finishes.

This simplistic example shows that the complexity of a Prolog program (execution), may be affected by the order in which literals appear in a clause. We will later see that even the output may be affected by this order.

Prolog programming

Instantiating variables

The question does Prolog permit side-effects is difficult to answer. On one hand:

Variables can be (partially) bound several ways:

Representations - lists

The idea

One distinguishing feature of FOL is that it allows representing objects which programmers use, such as lists, trees, graphs, matrices, etc. Prolog inherits this feature. In Prolog, we can enroll any term in a relation, including other - composite terms. At the same time, variables can be bound to terms. We illustrate this via an example:

-? X = f(Y,Z), Y = g(W), W = Z, Z = 2.

Here, X is bound to f(g(2),2). Note that f is a binary relation and g(2) (a term) is enrolled with 2 in this relation.

We can exploit this feature to represent lists as follows:

-? L = cons(1, cons(2, cons(3, void))).

Note here that:

We can define helpful relations over lists:

head(L,H) :- L = cons(H,_).
tail(L,T) :- L = cons(_,T).
 
empty(L) :- L = void.
nonempty(L) :- L = cons(_,_).

_ is used much in the same way as in Haskell - to designate an arbitrary term whose name is unimportant for the programmer.

The basic observation here is that:

We can add new relations such as len:

len(L,R) :- empty(L), R = 0.
len(L,R) :- nonempty(L), tail(L,T), len(T,R1), R is R1 + 1.

Note that, we have defined two clauses which treat terms of different types for L. We can rewrite len in several ways, which illustrate Prolog's flexibility:

len(void,R) :- R = 0.
len(cons(_,T),R) :- len(T,R1), R is R1 + 1.

We can perform unification implicitly in the left-hand side of the implication. We can also do it for R, which results in:

len(void,0).
len(cons(_,T),R) :- len(T,R1), R is R1 + 1.

Actual Prolog lists

In Prolog (as in Haskell), lists are an essential programming construct. In the previous section, we have illustrated the principle governing list representations (as relations). Lists are already implemented in Prolog and helpful relations (or predicates) are already defined for them.

Real Prolog lists are defined as follows:

[] % the empty list
[H|T] % 'cons' of H into T
[1,2,3] % a list with integers 1,2 and 3

Thus, in order to work on Prolog Lists, we could rewrite len as follows:

len2([],0).
len2([_|T],R) :- len2(T,R1), R is R1 + 1.

List membership

We start with the following implementation of the predicate contains which verifies if an element is part of a list:

contains(E,[E|_]).
contains(E,[H|T]) :- E \= H, contains(E,T).

Testing our predicate yields:

?- contains(1,[1,2,3]).
true 
false.

The second false seems puzzling and inconvenient. We can trace it down if we build the proof tree for contains(1,[1,2,3]):

contains(1,[1|_]) is satisfied. true is reported. Prolog continues re-satisfaction:
contains(1,[1|[2,3]]) may be satisfied:
  1 \= 1 is not satisfied, hence Prolog reports false. Re-satisfaction ends.

We shall examine a more efficient way of defining contains in a future lecture.

List reversing

We start with the following - accumulator-based implementation:

rev([],R,R).
rev([H|T],Acc,R) :- rev(T,[H|Acc],R). 
reverse(L,R) :- rev(L,[],R).

While -? reverse([1,2,3],L). works just as expected, reverse(L,[1,2,3]). reports the correct result and then loops. First of all, we note that this query violates our assumption the the first list is the input, while the second - the output. Again, to understand the behaviour, we build the proof tree:

to satisfy reverse(L,[1,2,3]), we must satisfy rev(L,[],[1,2,3]).
the first clause of rev cannot be satisfied since [] and [1,2,3] do not unify.
during the satisfaction of the second clause L = [H1|T1] (which is possible since L is unbound). 
  We attempt to satisfy rev(T1,[H1|[]],[1,2,3]):
    the first clause of rev cannot be satisfied ([H1] and [1,2,3] do not unify)
    while satisfying the second clause T1 = [H2|T2].
      We attempt to satisfy rev(T2,[H2|[H1|[]]],[1,2,3]).
         the first clause of rev cannot be satisfied ([H2,H1] and [1,2,3] do not unify)
         while satisfying the second clause T2 = [H3|T3].
            We attempt to satisfy rev(T3,[H3|[H2|[H1|[]]]],[1,2,3])
              the first clause of rev CAN be satisfied ([H3,H2,H1] unify with [1,2,3]). The result is reported
              we attempt re-satisfaction via the second clause: T3 = [H4|T4]
                the first clause of rev cannot be satisfied ([H4,H3,H2,H1] and [1,2,3]) do not unify).
                ....
                the process repeats until the stack becomes full.

Take-away