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).
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.
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)\}$
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):
?- student(gigel)
?- student(gigel),studies(gigel,aa)
(,
should be read as logical and)?- student(X)
:X
is a variable. All tokens starting with a capital letter are (treated as) variables.;
we can iterate over all such values.?- student(X), lecture(X,aa)
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).
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.
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.
The question does Prolog permit side-effects is difficult to answer. On one hand:
-? female(X), studies(X,L)
, initially, the variable X
is unbound. However, once the part female(X)
has been satisfied, X
is bound to mary
.X
remains permanently bound to Mary. X
can only become free upon resatisfaction of a query/clause.Variables can be (partially) bound several ways:
X = 2
is not the conventional assignment - =
designates unification. We illustrate this via several examples:-? X = 2, Y = X.
-? X = Y + 2, Z = 2 + Y, X = Z.
- here note that expressions are treated as such - no computation (of 2+2
) is performed.is
operator:-? X is 2+2.
=:=
:-? X is 2+2, X =:= 4.
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:
cons
is a binary relation. A list is an instance of such a relation.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:
L
is bound and stands for the inputH
and T
are unbound, and stand for the output. They will be bound to the result, or results, if there are several.-? L = cons(1,cons(0,void)), head(L,H).
-? head(L,1).
is valid, and will produce: L = cons(1, _G766).
Here, _G766
is an anonymous variable which is not bound - it can be anything. Basically, what Prolog is telling us is that L
must be cons of 1 into something.
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.
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.
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.
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.
contains
will be an example), however, the programmer must make sure this is indeed possible.