COS 441 - Logic Programming - April 30, 1996

Logic Programming

Logic programming, as represented as Prolog has its roots in Horn clause logic. The Horn clauses are a subset of the first order predicate calculus. A theory of a first order predicate calculus consists of: A first order language has:
               terms:   t ::= c | x | f( t[1] ... t[n] )
     atomic formulae:   a ::= p( t[1] ... t[n] )
well-formed formulae: wff ::= a | wff AND wff | wff OR wff | ~wff | wff -> wff
			        | FORALL x . wff | EXISTS x . wff
We don't need all of these connectives and qualifiers:
EXISTS x F = ~ FORALL x . ~F
           A AND B = ~(~A OR ~B)	
            A -> B = ~A OR B
In fact it is possible to transform any wff into clausal form (provided that sufficient Skolem functions exist):
FORALL x[1] ... x[n] . L[1] OR L[2] OR ... L[m]
where literals L[i] are a | ~a and x[1] ... x[n] are all the variables of L[1] ... L[m] (i.e. clauses have no free variables).

The Horn clauses are a subset of these where a clause has at most one non-negated literal. For example,

A OR ~B OR ~C or ~D   (equivalent to)  (B AND C AND D) -> A.
We write the non-negated literal first. Since all Horn clauses enjoy this regular structure we write them as:
A <- B C D   (or)    A :- B C D
A is the head of the clause and B, C, D are its sub-clauses (or subgoals). A Prolog program consists of: A solution to a Prolog program is a satisfying assignment to variables of the query that makes the query consistent with the facts and rules of the database. By convention uppercase indicates variables, lowercase everything else. An example follows.
female(ann).
female(mary).
female(diane).
parents(ann,fred,mary).
parents(diane,fred,mary).
parents(mary,john,liz).
sister(X,Y) :- female(X), parents(X,M,F), parents(Y,M,F).

?sister(ann,diane). (true)
?sister(ann,mary).  (false)
?sister(ann,X).     (true, X = {ann,diane})
?sister(X,diane).   (true, X = {ann,diane})
Executing a Prolog query is done in the following manner. Match is described as: Match: term x term -> assignment
match   X, X       empty set
	c, c       empty set
	c1, c2     fail       (c1 != c2)
	X, c1      [X |-> c1]
	c1, X      [X |-> c1]
	X, Y       [X |-> Y]
Look familiar? Its unify! When we fail, we go back to step 1 and try the next rule. If there are no more rules, fail the most recent subgoal.

To implement a prolog interpreter we keep a stack of goals that records how to continue the search in step 1 and how to reset variables bindings as they were where the search must be resumed.

    lhs: rule -> atomic
    rhs: rule -> list of atomic
   head: atomic -> predicate
vars-of: (rule or atomic) -> set of vars

(define stk (#f))
(define variables '())

(define search
  (lambda (goal)
    (letrec (loop ((lambda (db)
                     (cond ((null? db) (fail))
                     	   ((eq? (head goal) (head (lhs (car db)))))
                     	   (if (let/cc k
                     	          (set! variables (filter var? variables))
                     	          (push! stk (cons k variables))
                     	          (let ((rule (instance (car db))))
                     	          	  (set! variables (append vars-of rule
                     	          	  		   variables))
                     	                  (unify goal (lhs rule))
                     	                  (for-each search (rhs rule))
                     	                  #t))
                     	          #t
                     	          (loop (car db)))
                     	   (else (loop (cdr db)))))))
      (loop db))))
      
(define fail
  (lambda ()
    (let ((backtrack (pop! stk)))
      (set! variables (cdr backtrack))
      (reset-variables!)
      ((car backtrack) #f))))
      
(define query
  (lambda (q)
    (set! stk (empty-stack))
    (if (let/cc k
          (push! stk (cons k '()))
          (set! variables (vars-of q))
          (search q)
          (display answer)
          #t)
        #t
        (printf "no solution"))))
The list data type is represented in Prolog as [X | Y]. The first element is called the head and the last is called the tail. We can define member in the following way:
member(X,[X | _]).
member(X,[_ | Y]) :- member(X,Y)

?member(ann,[ann,diane,mary]). (true)
We can also define append:
append([],L,L).
append([X,L1],L2,[X | L3]) :- append(L1,L2,L3).

?append([a,b],[c,d],X).            (yes. X = [a, b, c, d])
?append(X,[c, d], [a, b, c, d]).   (yes. X = [a, b])

Prolog is NOT logic. One reason is that the order of rules in the database matters.

g(foo).
p(X) :- p(X).
p(X) :- q(X).
?p(Z).
This yields an infinite loop. But swapping the two rules for p(X) it will terminate with the correct answer.

It is not always possible to arrange rules in such a way as to ensure termination. Hence Prolog adds a "cut" operator, denoted by the exclamation point. It controls search order. A clause of the form:

h(X) :- f(X), !, g(X)
indicates that once f(X) is satisfied, the binding for X is fixed and can not be undone by backtracking. The cut operator cuts off backtracking through the proceeding subgoals. Cut has at best tenuous connections with logic.

Exercise