Logic programming

The primary virtue of logic programming is its declarative semantics. A logic program can be read as a theory stating relations between entities in a particular domain. For a programmer, such an interpretation allows to separate the concerns for the logical structure of an algorithm from issues of control. The famous phrase [M] algorithm = logic + control of  [Ko79] states this principle succinctly. The idea of using predicate logic as a programming language arose from research in automated theorem-proving in the early 70s and resulted in the language Prolog. The first implementation was by Roussel/Colmerauer. Soon afterwards, efficient implementations became available which demonstrated the fruitfulness of this idea, at least for the kind of problems to be found in academic settings. By now Prolog is a widely accepted programming tool which is applied in areas like databases, problem solving, natural language processing, compiler design and, not the least important, expert systems. According to  [Bu83] Prolog has often been used to prototype small- to medium-scale expert systems in a business environment. As another indication of the potential of the paradigm, it may be mentioned that the Japanese Fifth Generation Computers project is based on logic programming. The logical language used in logic programming is called Horn clause logic, which is a subset of predicate logic that enables an efficient computational interpretation. C.f.  [DoGa84]. \nop{ Polynomial time/pebblings. } In this section, which is necessarily of an introductory nature, we will explore the notion of a logic program and its mathematical, logical foundations enabling a declarative reading of a program. Complementary to the declarative interpretation we will define a procedural interpretation that allows logic programs to be used for computing.\ftn{ Readers not interested in the mathematical foundations of logic programming are advised to jump to section \ref{dig/prolog}. } We will then describe the language Prolog, including the so-called impure or extra-logical facilities that are in practice considered necessary for using Prolog in actual programming tasks. We will defer a discussion of the merits of Prolog from a software engineering perspective to section softw.

Declarative versus procedural semantics

Logic is an excellent vehicle for reasoning about the state of affairs in a particular world. The advantage of logic is that it offers a natural formalism to express the facts and rules that pertain to that world. We will explain how such facts and rules can be stated in a logic program. Our treatment is based on  [Ll84]. \prologindex{declarative semantics} .so programs formulas .so models .so procedural

The logical variable

\prologindex{logical variable} Apart from being a means to establish the satisfiability of a goal, the power of logic programming lies in the way values are computed during an inference. The output of a goal with variables is a substitution binding these variables to terms. Terms are the elements of the universe a logic program deals with. As we have seen in section programs, defining logic programs, terms are either constants, variables or compound terms consisting of a function-symbol and zero or more argument-terms. We may use a logic program to define terms in a formal way. The program \oprog{terms}{
   constant(0) <-
   term(X) <- constant(X)
   term(s(X)) <- term(X)
  
} assumes a constant 0 and a one-argument function-symbol s and defines terms in accordance with the definition given earlier. The goal <- term(X) has as solutions all the possible bindings of X to the terms contained in the set [] { 0, s(0), s(s(0)),... } which represents the so-called Herbrand universe of the program terms. The possible output that may result from evaluating the goal <- term(X) is given by the substitutions [] {X/0}, {X/s(0)}, {X/s(s(0))},... binding X to the elements of the Herbrand universe. The question that we will answer in this section is how we are able to find these substitutions.

Substitutions

\prologindex{substitutions} Recall that a substitution %h is (represented by) a set of the form {X1/t1,...,X_k/t_k} that binds each variable X_i to a term t_i, for i=1,...,k. Applying a substitution %h to a term is recursively defined by \hspace{0.7cm} \begin{tabular}{l l} c %h = c & for a constant c, \\ X %h = t & for %h = {...,X/t,...} and X otherwise, and \\ f(t1,...,t_n) %h = f(t1%h,...,t_n%h) & for a compound term f(t1,...,t_n) \end{tabular} In other words, applying a substitution to a constant has no effect. Applying a substitution %h to a variable X results in the term t when the binding X/t occurs in %h. Applying a substitution %h to a compound term f(t1,...,t_n) results in the term f(t1 %h,...,t_n %h) in which %h is applied recursively to the argument terms t1,...,t_n. As an example, applying the substitution %h = {X/s(0)} gives [] $0%h = 0, X %h = s(0), Y %h = Y, and s(X)%h = s(s(0)) The application of a substitution is easily generalized to literals, by applying the substitution to each argument of the atom, and to conjunctions of literals, by applying the substitution to each literal. A substitution %h_2 is incompatible with a substitution %h_1 if there is a binding X/t1 in %h_1 and a binding X/t2 in %h_2 for which t1%h_2 != t2. For %h_2 compatible with %h_1, the composition %h_1 %h_2 of the substitutions %h_1 = {{X1/t1,...,X_n/t_n}} and %h_2 = {{Y1/t1',...,Y_k/t_k'}} is given by the set {{X1/t1%h_2,...,X_n/t_n%h_2,Y1/t1',..., Y_k/t_k'}}. If %h_2 is not compatible with %h_1, we say that the composition %h_1 %h_2 does not exist. For an arbitrary term t it holds that $(t%h_1)%h_2 = t ( %h_1 %h_2 ). Moreover, it is easy to check that the composition of substitutions is associative, that is that $(%h_1 %h_2) %h_3 = %h_1 (%h_2 %h_3). As an example, consider the composition of %h_1 = {{ X/s(X1) }} and %h_2 = {{ X1/s(X2) }} which results in %h_1 %h_2 = {{X/s(s(X2)), X1/s(X2)}}.

Unification

\prologindex{unification} Substitutions are the result of unifying two terms. A substitution
%h is a unifier of the terms t1 and t2 whenever t1%h = t2%h, that is when the terms become equal after applying %h. The most general unifier of two terms is the smallest substitution unifying the two terms. For example, the substitution %h = {{X/s(0)}} is the most general unifier of the terms f(X,Y) and f(s(0),Y). However, the substitution %h' = {{X/s(0), Y/0}} is also a unifier, but clearly less general since it may be derived from %h by adding the binding for Y. in other words, a substitution %h is called the most general unifier of two terms, or mgu for short, if for each unifier %s of these two terms there is a substitution %g such that %s = %h %g. A most general unifier can always be refined by another substitution to give an arbitrary unifier. Most general unifiers are not necessarily unique, but may be identified by renaming variables. We will describe a simple recursive algorithm to decide whether two terms are unifiable and to compute the most general unifier if it exists. To indicate that two terms are not unifiable we use the value fail. We now write the composition of substitutions %h_1 and %h_2 explicitly as %h_1 \c %h_2 and adopt the convention that %h_1 \c fail = fail and fail \c %h_2 = fail. Also when %h_2 is incompatible with %h_1, because they disagree on the binding for a variable, we define the composition %h_1 %h_2 = fail. We will use the constant %e to denote the empty substitution, for which it holds that %h \c %e = %e \c %h = %h for arbitrary %h. The algorithm is given by the following recursive equations [D unify(c1,c2) = %e if c1 = c2, unify(X,t) = {{X/t}} if X does not occur in t, unify(t,X) = unify(X,t) if t is not a variable, unify(f(t1,...,t_n), f(t1',...,t_n')) = unify(t1,t1') \c ... \c unify(t_n,t_n'), and unify(t1,t2) = fail otherwise D] Unifying two constants results in the empty substitution whenever the constants are equal. In case one of the terms is a variable X, a substitution binding X to the other term is delivered, provided that X does not occur in that term. Unifying two compound terms is possible only when the two terms have the same function-symbol and the same number of arguments. The result is the composition of the substitutions resulting from the pairwise unification of the argument terms. This leads to failure whenever such unification proves to be impossible or an incompatibility arises. The unification function delivers fail when none of these cases apply. As examples consider [D unify(p(s(X),0), p(Y,Z)) = {{Y/s(X)}} \c {{Z/0}} = {{ Y/s(X), Z/0 }} unify(p(s(X),0), p(Y,X)) = {{Y/s(X)}} \c {{X/0}} = {{ Y/s(0), X/0 }} unify(p(s(X),0), p(Y,s(Z))) = {{Y/s(X)}} \c fail = fail unify(p(s(X),0), p(Y,Y)) = {{Y/s(X)}} \c {{Y/0}} = fail D] In the last example fail ~ results because an incompatibility arises between the substitutions resulting from unifying the argument terms, since they disagree on the binding of the variable Y.

The occur-check

\prologindex{occur check} In the unification algorithm, a binding results whenever we encounter a variable X and a term t, provided that X does not occur in t. In actual logic programming systems this so-called occur-check is often omitted for reasons of efficiency. This may lead to anomalous behavior, as exemplified by the goal
<- X = s(X) which succeeds, resulting in the binding {{X/s(X)}}, although it clearly has no solution.

Compound terms

\prologindex{compound terms} In logic programming systems, unification provides a uniform mechanisms for parameter passing, data selection and data construction. Terms can be used to package data in a way resembling records. For instance, the fields on a chessboard can be denoted by the terms []
position(1,1), position(1,2),..., position(8,8) naming the index in respectively the row and the column of the board. In a program the pattern of this structure can be used to select the wanted information, as illustrated in the clause that tests whether two positions occur on the same row. \oprog{row}{
   same_row(position(X,Y), position(X,Z)) <- 
  
} When evaluating a goal {\em same_row} containing two positions, the information concerning the rows is selected and the implicit constraint that the rows are equal, since they are both referred to by the variable X, is enforced by the unification procedure.

The language Prolog

\label{dig/prolog} Prolog is the most widely used logic programming language that exists today. It implements a logic programming system as treated in the previous sections, but in addition contains a number of features that are convenient when programming actual systems. .so syntax lists bags arith cuts neg control meta dynamic $=

Examples

Concluding our discussion of Prolog, two examples will be given that will illustrate how to use Prolog for implementing search in a finite search space. The first example presents a solution to a problem in chess, the N-queens problem. Our solution is given for N = 8, but may be easily generalized to other values of N. The second example illustrates depth-first search with a loop-check built-in to prevent non-termination. .so queens states