====== Formal syntax, Unification, Backtracking, Cut, Negation ====== ===== Prolog syntax ===== The basic programming building block in Prolog is the **clause**. To define it formally, we first introduce: ::= alphanumeric sequence starting with capital ::= | | | ( ..., ) The definition of **terms** is slightly restrictive. For instance, terms are also arithmetic expressions (e.g. ''1 + X''), list //patterns// (e.g. [1|[]]), etc. But each such term can be expressed as a **predicate**, as already illustrated for lists, in the previous lecture. A clause is defined via the following grammar: ::= :- , ..., . ::= (, ... ) ::= (, ... ) | = | // unification is | // arithmetic assignment =:= | // arithmetic comparison true | // default satisfying goal fail | // default-failing goal ! | // cut (discussed below) Let: q(X,Y) :- r(X), p(Y). serve as an example. Then ''q(X,Y)'' is called **resolvent**, or **goal**. Also, in, any query e.g. ''-? q(X,Y).'', ''q(X,Y)'' is termed **goal** (which Prolog tries to //prove//). ''r(X)'' and ''p(Y)'' are called sub-goals (which Prolog needs to prove, in order to prove ''q(X,Y)''. There exist special coals, such as ''='' (unification), ''is'' (assignment) or ''=:='' (comparison): * the goal '' = '' will be satisfied iff the two terms unify. Under unification **variables become bound by a substitution**. * the goal '' is '' will be satisfied iff there is no type error, and will result in binding the variable to the **evaluation** of the term. * the goal '' =:= '' will be satisfied iff **the evaluation of each term** yields the same value. We also note that the above syntax is not complete, but covers most of the Prolog programming aspects of this lecture. ==== Shorthands ==== Clauses such as: P(X,Y) :- true. can also be written as: P(X,Y). Also, a clause which involves unification such as: P(X) :- X = f(a). can also be written as: P(f(a),Y). Shorthanded unification is often used for lists, and looks similar to Haskell pattern matching, e.g. size([],0). size([_|T],R) :- size(T,R1), R is R1 + 1. ==== Common pitfalls ==== The following two clauses: ... :- ..., X=p(A,B) , ... ... :- ..., p(A,B) , ... may look similar, but their subgoals are actually very different: * the first is satisfied if the unification of ''X'' with ''p(A,B)'' succeeds (hence ''p(A,B)'' is merely a term here). * the second is satisfied if ''p(A,B)'' is satisfied. ===== Unification ===== We write $math[t =_S t'] to express that //term $math[t] unifies with $math[t'] under substitution $math[S]//. A **substitution** is a set of pairs $math[(X,t)], where $math[X] is a variable and $math[t] is a term. As a shorthand, we write $math[t/X] instead of $math[(X,t)]. A **substitution** $math[S] is **consistent** iff for all pairs $math[t_1/X] and $math[t_2/X], $math[t_1 =_S t_2] (the variable $math[X] cannot be bound to different terms which do not unify). ==== Unification rules ==== In what follows, we give some basic unification rules which serve the purpose of illustration. These rules are not implemented per.se. in the Prolog unification process. $math[\displaystyle\frac{}{atom =_S atom}] $math[\displaystyle\frac{S \cup \{t/X\} \text{ is consistent }}{X =_S t}] For instance, $math[X=_{\{cons(H,T)/X\}} cons(1,void)] since $math[\{cons(H,T)/X, cons(1,void)/X\}] is consistent. However $math[X =_{\{void/X\}} cons(1,void)] is false ($math[X] cannot at the same time be the empty list, and a list with one element). $math[\displaystyle\frac{p_1=p_2, n = m, t_1 =_{S_1} t'_1, \ldots, t_n =_{S_1} t'_n, S = S_1 \cup \ldots \cup S_n \text{ is consistent} }{p_1(t_1, ..., t_n) =_S p_2(t'_1, \ldots, t'_m)}] For instance: $math[p(a,q(X,Y),Z) =_S p(X,q(Y,Y),q(X))], where $math[S] is $math[\{a/X,a/Y,q(a)/Z\}]. The unification algorithm from Prolog will compute **the most general substitution** or **the most general unifier** (MGU). In this lecture, we will not provide a formal definition for the MGU. However, we illustrate the concept via an example. $math[p(X,q(X,Z)) =_S p(Y,q(Y,Z)] for $math[S = \{Y/X,X/Z\}] however $math[S] is not an MGU. The constraint that $math[Y] unifies with $math[Z] is superfluous. Here, an MGU is $math[S = \{Y/X\}]. ==== Recursive substitutions ==== Does ''X=f(X)'' produce a valid substitution? Intuitively, such a substitution would contain $math[f(f(...))/X]. Prolog's unification algorithm is able to detect such recursive substitutions and will not loop. **Depending on the Prolog implementation (version)** at hand, the unification may fail or succeed. ==== Goal satisfaction (Backtracking) ==== During the satisfaction of a goal, Prolog keeps a current substitution which it iteratively updates. When a sub-goal is satisfied, a new "current substitution" is created. Let us look in more depth at this. contains(E,[E|_]). contains(E,[H|T]) :- E \= H, contains(E,T). The execution tree for the goal ''-? contains(2,[1,2,3]).'' is given below: contains(2,[1|[2,3]]) 2 \= 1 contains(2,[2|_]) -> true (continue goal satisfaction) contains(2,[2|[3]]) 2 \= 2 -> false. (re-satisfaction not possible). Alternatively, let us look at how ''member'' is implemented: member(X,[X|_]). member(X,[_|T]) :- member(X,T). The execution tree for ''member(2,[1,2,3])'' is shown below: member(2,[_|[2,3]]) member(2,[2|_]) -> true (continue goal satisfaction). member(2,[_|[3]) member(2,[_|[]]) member(2,[]) -> false (re-satisfaction not possible). The difference between ''contains'' and ''member'' is that, while the former stops once an element in the list has been found, the second continues search. Is there a good motivation for the ''member'' implementation? * build a goal tree for ''member(X,[1,2,3])'' * build a goal tree for ''contains(X,[1,2,3])''. === Backtracking === Note that goal satisfaction in Prolog is similar to //pruned backtracking//. ==== Cut (!) - pruning the goal tree ==== Consider the following program: f(a). f(b). g(a). g(b). q(X) :- f(X), g(X). The proof tree for ''-? q(X)'' is: f(X) X = a (subgoal satisfied). Current substitution is {a/X} g(X) g(a) true. The goal q(X) is satisfied under {a/X}. Attempt re-satisfaction ... ... resatisfaction not possible for g(X) under {a/X}. X = b. Current substitution is {b/X} g(X) g(b) true. Attempt re-satisfaction.... ... not possible not possible to re-satisfy f(X). not possible to re-satisfy q(X). stop. In Prolog, the special goal //cut// written ''!''is satisfied according to the following rules: - when it is first received, it is **implicitly satisfied**. - when there is an attempt to **re-satisfy it**, it **fails**. Let us modify the previous clause to: q(X) :- f(X), !, g(X). The corresponding proof tree is: f(X) X = a (subgoal satisfied). Current substitution is {a/X} ! (implicitly satisfied) g(X) g(a) true. The goal q(X) is satisfied under {a/X}. Attempt re-satisfaction ... ... resatisfaction not possible for g(X) under {a/X}. X = b. Current substitution is {b/X} ! (implicitly fails). q(X) cannot be re-satisfied. stop We turn to a more elaborate example: f(a). f(b). g(a,c). g(a,d). g(b,c). g(b,d). q(X,Y) :- f(X), !, g(X,Y). q(test,test). The example illustrates a side-effect of the cut semantics: ''q(X,Y)'' will not be satisfied for $math[\{test/X,test/Y\}]. This shows that the effect of cut also extends **to the current goal**, not only **the current clause of the current goal**. Similarly, we have: q(test,test) :- !. q(X,Y) :- f(X), !, g(X,Y). where cut prevents the re-satisfaction of ''q'' via the second clause (the second cut will not even be reached). Also, consider the following code which extends the previous example: r(X,Y) :- q(X,Y). r(1,2). The goal ''-? r(X,Y)'' will be satisfied for $math[\{1/X,2/Y\}], which shows that cut does not have any effect on ''r'' - thus, cut should not be interpreted as a //**global proof tree pruner**//. ==== Negation ==== Prolog implements negation as **negation-as failure**. This is also called **the closed-world assumption**. In short, the negation ''not(G)'' should be interpreted as //G cannot be proved in the current program//. Note that, in First-Order Logic, the negation of a sentence $math[p] is interpreted more generally: * it has its own //proof tree// which is independent of that for $math[p] * it is possible to have //theories// (programs) where both $math[p] and $math[~p] are true. Such theories are called **inconsistent**. It is also possible that $math[p] is provable while $math[~p] is not. Such a theory is called **incomplete**. The Prolog implementation of not is given below: not(G) :- G,!,fail. not(_). It is also a good illustration of a **design pattern** for logical programming, which is often used in Prolog: * The key property of ''not'' is that **it satisfies without modifying the current substitution**, **even if G may bound variables**: * In the first clause, if G is satisfied, cut is reached for the first time (satisfies). Subsequently failure occurs and re-satisfaction is prevented. The second clause is never reached. * However, if G is not satisfied, the cut is never reached, and ''not'' succeeds trivially, **without binding any variable from G**. The reason while ''X \= H'' fails in our ''contains'' example is that it is actually a syntactic sugar for ''not(X = H)''. More precisely, note that, in: ''-? H = 1, not(X = H).'' * ''X = H'' succeeds hence ''not(X = H)'', fails. * re-satisfaction of ''not(X = H)'' is not possible, hence any supergoal having ''not(X = H)'' on a branch will fail also.