====== 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 $math[A_1, \ldots, A_n], having **arity n**, is a subset $math[R\subseteq A_1 \times \ldots \times A_n]. In Prolog, we make no distinction between the types ($math[A_i]) of each element from a relation. Thus, in Prolog, **a relation** of **arity n** is a subset $math[R\subseteq Prim] where $math[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, :$math[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: $math[R(t_1,\ldots, t_n)] and $math[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. * Variables can be **bound** to any value from the program. Variables do not have a type. * When a variable is present in a query, Prolog will try to find **all values** for that variable which satisfy the query. By typing '';'' we can iterate over all such values. * ''?- student(X), lecture(X,aa)'' ==== 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: $math[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: $math[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: $math[\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: * side-effects are present: **variables can be instantiated**. For instance, when resolving ''-? 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''. * however, **side-effects are inherently limited**: during **the satisfaction of a query/clause, a bound variable cannot be unbound**. In our example, ''X'' remains //permanently bound to Mary//. ''X'' can only become free **upon resatisfaction of a query/clause**. Variables can be (partially) bound several ways: * via **unification**: e.g. ''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. * conventional assignment is performed using the ''is'' operator: * ''-? X is 2+2.'' * comparison is performed using the operator ''=:='': * ''-? X is 2+2, X =:= 4.'' ==== 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: * ''cons'' is a binary relation. A list is an **instance** of such a relation. * it enrols an **element** (the head) together with a **relation instance** describing the **tail**. 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 use queries over relations in order to perform computations on lists** * we assume variable ''L'' is **bound** and stands for the input * we assume variables ''H'' and ''T'' are unbound, and stand for the output. They will be bound to the result, or results, if there are several. * example: ''-? L = cons(1,cons(0,void)), head(L,H).'' * essentially, this is a **programmer convention**. For instance, the query: ''-? 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. === 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 ==== * Building and understanding //proof trees// is an essential tool for learning to program safely in Prolog. * It is important to keep a convention regarding //input// and //output// variables when writing a Prolog program. Sometimes, such a convention can be more flexible (''contains'' will be an example), however, the programmer must make sure this is indeed possible.