Logic Programming and Prolog

(edited from http://www.cs.uoregon.edu/classes/cis425/week10.html)


Subjects


Concept

Here is a concept to consider:
   > val x = [1, 2, 3];

   > val y = [1, 2, ?];  (* partial list *)

   > y := [1, 2, 3, ?];  (* increases info *)

   > y := [1, 5];        (* contradiction *)
The last statement is not legal. This is because y has already been established as a "list" starting with 1 and 2, so the 5 is not legal. We cannot change our mind about the given facts.

The ? indicates the possibility of more information. The third line shows how the information can be increased.


Relations versus Functions

In logic programming we talk about relations rather than functions.

Consider the append function:

   - fun append (nil, x) = x
       | append (hd::tl, x) = hd::append(tl, x);

   - append([1, 2], [3, 4]);
   val it = [1, 2, 3, 4] : int list
But what if we want to find the result to the following query?
 
   - append ([1, 2], x) = [1, 2, 3, 4]
The desired result would be [3, 4] ; we want the system to fill in the missing information. ML won't know that we want to find the result of x in this case. We would have to write another function that dealt with this particular query.


Another way to look at this concept involves the following definition:
   - append relation(X, Y, Z):
     []       [1]    [1]
     [1]      []     [1]
     [1, 2]   [3]    [1, 2, 3]
      .        .      .
      .        .      .
Where the relation is defined on an infinite set of tuples.

We could then ask a number of questions to see if the desired result exists:

 
   append([], [], [])?  yes 
   append([], [1], [1])?  yes 
   append([1,2], [3], [1,2,3])?  yes 
   append([], [], [])?  yes 
What if we ask the following?
 
   append(x, [1], [1])?
   append([1], x, [1])?
   append([1], [], x)?
In the first case, X is an output where Y and Z are inputs. In the second case, Y is an output where X and Z are inputs. In the third case, Z is the output while X and Y are inputs.

These all ask a similar question: is there an x such that the given pattern is in the relation append ? The output would be a possible result for x, if one existed. Subsequent queries would return other possible answers until all are exhausted.


The key to logic programming is you do not need to specify exactly how a result is computed. You simply need to describe the form of the result. It is assumed that the computer system can somehow determine how the result is computed.

With this type of language, it is necessary to provide both the relevant information and a method of inference for computing the results.

Arguments and results are treated uniformly. This means there is no distinction between input and output.

It is also important to note that Prolog is NOT a typed language.


Facts and Rules

Logic programming describes information in terms of facts and rules.


Fact

A fact is analogous to a base case in a proof. It is a piece of information that is known and is a special case of a rule. A fact holds without any conditions, i.e. they are propositions that are assumed to be true.

Facts are also known as unconditional Horn Clauses. The general case of a Horn clause is:

 
   P if Q1 and Q2 . . . and Qn
where a fact is simply P with no conditional statements Q1, Q2 . . . . Note that horn clauses CANNOT represent negative information. That is, you cannot ask if a tuple is NOT in a relation.


Rule

A rule describes a series of conditions followed by a conclusion returned if the conditions are met. A rule is analogous to inductive steps in proofs.

Rules are also known as conditional Horn clauses.


Some Prolog examples:

Fact:

 
   append([], X, X).
This says that if you append [] and something, you will obtain something.

Rule:

 
   append([a, b], [c, d], [a, b, c, d]) if append([b], [c, d], [b, c, d]).
In Prolog, capital letters are variables and lower case letters are atoms. A "." (period) is used to end a statement. Another notation convention is described by: [a, b] = [a | [b]] where the | operator is equivalent to cons (i.e. it separates the head and tail of a list).

The above rule says: if the condition is met, return the appended result of the given lists.


Goals

A goal is simply a question we want answered by the system based on the knowledge in the database of information. Creating facts and rules builds our database of information. We can then state goals and get results back based on the database.

Prolog:

 
   append([], X, X).    <----- fact
   append([H | X1], Y, [H | Z]) :- append(X1, Y, Z).   <---- rule
This is our database of knowledge. Now we can state goals to see if the information resides in the relation(s) created.

Note that the ":-" operator means "if" and tells us we are creating a rule. (Thus, P :- Q means P if Q .)

 
   ?- append([a, b], [c, d], [a, b, c, d]).
   yes
This goal was found in the relation.
 
   ?- append([a, b], [c, d], Z).
   Z = [a, b, c, d] ?
   yes
We query the system and it returns the first result it comes across in the relation that satisfies the value of Z.

The question mark after the result is the system asking if we want to search for more answers to our goal. Pressing enter means that we don't want to search for more answers. The system will respond that "yes" or "no", there are more answers that exist.

Similar to the above query:

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

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

After obtaining an answer to a goal, if the user responds to the "?" prompt with a semi-colon, the next answer will be returned if any other answer exists. A "no" will be returned if all possibilities have been exhausted.

A fact and a rule:

 
   append([], X, X).
   append([H | X], Y, [H | Z]) :- append(X, Y, Z).
Goals:
 
   | ?- append([a], [b, c, d], X).
   X = [a, b, c, d] ? ;    <--------- ";" means show more answers
   no      <--------- the system says there are no more answers

   | ?- append(X, Y, [a, b, c, d]).
   X = [],
   Y = [a, b, c, d] ? ;    <--------- show more answers 

   X = [a],
   Y = [b, c, d] ? ;

   X = [a, b],
   Y = [c, d] ? ;

   X = [a, b, c],
   Y = [d] ? ;

   X = [a, b, c d],
   Y = [] ? ;
   no      <--------- the system says there are no more answers

Another example:
 
   append([], X, X).
   append([H | X], Y, [H | Z]) :- append(X, Y, Z).

   | ?- append(X, X, [b, c, b, c]).
   X = [b, c] ? ;
   no
This is pretty straight forward. But what happens when we ask this:
 
   | ?- append([b, c], X, X).
   X = [b, c, b, c, b, c, b, c, b, c, b, c, b, c, b . . . .
We get an infinite list.
Relations can be defined on other relations.

For instance, the prefix relation is defined:

 
   ? prefix(X, Z) :- append(X, Y, Z).
And the suffix relation can be defined:
 
   ? suffix(X, Z) :- append(Y, X, Z).


How logic programming works

Logic programming uses facts and rules to represent information. Deduction is used to answer queries.

The Kowalski definition of Logic Programming is abstracted to:

 
   ALGORITHM = LOGIC + CONTROL
The LOGIC is provided by the user/programmer and the CONTROL is derived by the system based on the LOGIC database of information.

In other words, we talk about programming in terms of deduction rather than evaluation.


A rule is defined:
 
   P :- Q1, Q2, . . ., Qn
where the goals Q1, Q2, . . . Qn are executed from left to right.


More on facts

Prolog is interactive like ML and Scheme. You can also create a file of facts and load them from the command line. Assume that the following facts are in a file called facts .
 
   father(john, mary).
   father(sam, john).
   father(sam, kathy).
Now, we start Prolog, load the file, and state some goals/queries:
 
   sicstus                     <-------- command to start Prolog
   SICStus 2.1 *8: Wed Apr 28 18:33:10 PDT 1993
   | ?- [facts].               <-------- load the file: facts
   | ?- father(john, mary).
   yes
Our interpretation of the father relation father(X, Y) is that X is a father of Y .

So, when we query the system if john is the father of mary , the answer we get back is yes . Indeed, that fact is in our information database because of the file we loaded.

Now, we want to know if sam has any children:

 
   | ?- father(sam, X).
   X = john ? ;  <-------- are there any more?
   X = kathy ? ;
   no
Sam has two children: john and kathy .

Who is the father of mary?

 
   | ?- father(X, mary).
   X = john ? ;
   no


Given the same fact file from above, how are these goals evaluated:
 
   | ?- father(X, john), father(X, kathy).
   X = sam ? ;
   no

   | ?- father(X, john), father(X, Y).
   X = sam,
   Y = john ? ;
   X = sam, 
   Y = kathy ? ;
   no
The goals are stated in terms of two sub-goals which are "and"-ed together. The system sequentially searches the database of information from the top down. One pointer for each sub-goal traces through the database until matches that satisfy both sub-goals are reached.

In tracing the second example, we start out by trying to satisfy the first sub-goal father(X, john) . Starting at the top of the database, we look for a pattern that matches the sub-goal. The second item in the database, father(sam, john) matches. Thus, X = sam now.

Next, we try to satisfy the second sub-goal father(X, Y) which is actually father(sam, Y) now. We again begin tracing through the database looking for matches. The first match encountered is father(sam, john) . At this point, the entire goal has been satisfied and the result X = sam, Y = john is returned.

We enter a semi-colon which tells the system we want to look for another possible solution that satisfies our goal.

The system continues tracing through the file attempting to find a match to the goal father(sam, Y) . Another match is found at father(sam, kathy) . The goal has been satisfied again and the result X = sam, Y = kathy is returned.

When prompted for another possibility, the system continues searching for a match to the second sub-goal. There is no more information left in the database at this point. However, the pointer to the first sub-goal is still at father(sam, john) . We haven't exhausted all possibilities for the first sub-goal yet.

Therefore, we continue searching through the database looking for a match to the goal father(X, john) . None is found, so we are done. "No" is returned.

If another match had been found for the first sub-goal, we would look at the second sub-goal again starting from the beginning of the database.


Following from the same example above, what happens if we do the following:
 
   | ?- father(X, john), father(X, Y), Y \== john.
   X = sam, 
   Y = kathy ? ;
   no
The operator \== tells the system to ignore any matches where Y equals john . Thus, we eliminate the first solution we had in the previous example.


Another example:
 
   father(john, mary).
   father(sam, john).
   father(sam, kathy).

   | ?- father(X, Y), father(X, Z).

   X = john,
   Y = mary,
   Z = mary, ? ;

   X = sam,
   Y = john,
   Z = john ? ;

   X = sam, 
   Y = john, 
   Z = kathy ? ;

   X = sam,
   Y = kathy, 
   Z = kathy ? ;
   no


More on Rules

A rule is defined in the following way:
 
   <term> :- <term1>, <term2>, . . . . . , <termn>
        ^                              ^
        |                              |
       HEAD                        CONDITIONS
The head is the conclusion and the conditions are considered to be "and"-ed together. Note: A fact is just a special rule with no conditions. Example:
 
   father(john, mary).     <------ facts
   father(sam, john).
   father(sam, kathy).

   grandpa(X, Y) :- father(X, Z), father(Z, Y).  <------ rule

   | ?- grandpa(X, Y).
   X = sam,
   Y = mary ? ;
   no
Without going into too much detail, the first condition of the rule will essentially match on anything in the given database of information. The important step is in the second condition as we try to satisfy the goal.

We essentially are looking for a match where one person is both a father and a child (variable Z) to two other people. These two other people are then returned as the grandpa and grandchild.

For each value of Z we encounter in the first condition we must then match that Z value to the other position in the father relation for the goal to be satisfied.


Unification

Two terms unify if they have a common instance (term) U between them. Deduction in Prolog is based on unification.

Unification is similar to pattern matching. However, there is a difference because pattern matching can only happen one way, from left to right. Unification can match both ways; it depends on where the variables and the atoms are.

 
   | ?- X is 2+3.
   X = 5 ?
X is instantiated to the value 5. The expression 2+3 is evaluated because we are using the operator "is" which tells us to evaluate 2+3 and make X that value.

To unify, we use the operator =.

 
   | ?- X = 2 + 3.     (2+3=X)        +
   X = 2 + 3 ?                       / \
                                    2   3
The diagram on the right is the term that X is unified with.
 
   | ?- 5 = 2 + 3.
   no
This goal is NOT unifiable.
 
   | ?- 2 + 3 = 2 + Y.         +           +
   Y = 3 ?                    / \         / \
                             2   3       2   Y
This goal is unifiable. We are increasing the information of Y to equal 3.
 
   | ?- f(X, b) = f(a, Y).        +           +
   X = a,                        / \         / \
   Y = b ?                      X   b       a   Y
This goal is unifiable because we can match X to a and Y to b .


Another example:
 
   | ?- 2*X = Y*(3+Y).      *            *
   X = 3 + 2,              / \          / \ 
   Y = 2 ?                2   X        Y   +
                                          / \
                                         3   Y
The Y on the left is unified with the 2 on the right. The X on the left is the unified with (3 + Y) on the right which is actually (3 + 2) after the unification of Y with 2.


Infinite loop examples:
 
   | ?- X = X + 2.
   X = 
   Prolog interruption (h for help)? a
   {Execution aborted}
What happened? The X on the left is infinitely matched to X + 2 on the right.
 
   | ?- X = 2 + X
   X = 2+(2+(2+(2+(2+(2+(2+(2+(2+(2+(2+(2+(2+(2+(
   Prolog interruption (h for help)? a
   {Execution aborted}
What happened? The same thing happened as above, except it knew what to do with each instance of 2+( that it found. The problem was that the unification for X still went on infinitely.


Other unification examples:
 
   | ?- X is 2 + 3, X = 5.
   X = 5 ?

   | ?- X is 2 + 3, X = 2 + 3.
   no

   | ?- 2 + 3 is X.
   ERROR.

   | ?- X is 5, 6 is X + 1.
   X = 5 ?

   | ?- X = 5, 6 = X + 1.
   no
 
   | ?- 6 is X + 1, X = 5.
   ERROR.

   | ?- Y = 2, 2*X is Y*(Y+3).
   no

   | ?- Y = 2, Z is Y*(Y+3).
   Y = 2, 
   Z = 10 ?


Lists

Notation:
 
   [a, b, c] = [a, b, c | []] = [a | [b, c]]
This simply shows how lists are constructed with a head and a tail.


Here, we try to unify two lists:
 
   | ?- [H | T] = [a, b, c].
   H = a, 
   T = [b, c] ?
 
   | ?- [a | T] = [H, b, c].
   H = a,
   T = [b, c] ?
The head and tail is matched and the values are output.


Trees

Here is a definition for a binary tree in ML:
 
   - datatype bintree = empty | node of int * bintree * bintree;

   - fun member(k, empty) = false
       | member(k, node(n, s, t)) =
            if k < n then member(k, s)
            else if k < n then member(k, t)
            else true;
How might you define a binary tree in Prolog?
 
   member(K, node(K, _ , _ )).   <------ fact
   member(K, node(N, S, _ )) :- K < N, member(K, S). <---- rules
   member(K, node(N, _ , T)) :- K > N, member(K, T).
Note: the underscore "_" is used to "match" anything.

The fact defines the general structure of a tree -- a node with two branches. The first rule only worries about one side of the tree and searches that side of the tree if the value we are searching for is less than the current node.

Similarly, the second rule only worries about the other side of the tree and searches that side of the tree if the value we are searching for is greater that the current node.

This is a nice use of this language. It isn't even necessary to declare a special datatype to handle the tree; you simply write facts and rules to handle the data in the way you want to traverse the trees. The rest is handled by the system through deduction.


More Examples

Starting with the following fact and rule:
 
   member(M, [M | _]).
   member(M, [_ | T]) :- member(M, T).
The fact looks at the next item in the list to see if it exists. The rule checks to see if the item M is in the rest of the list, i.e. T.

What does the following return?

 
   | ?- member(a, [a]).
   yes
This matches to the fact. What about this:
 
   | ?- member(a, [b])
   no
This fails at the fact because the first item in the list is b not a . The condition in the rule also fails because the tail of the list [b] is just b which won't satisfy the fact. Thus, there is no match in the relation.


Order in the rules

The order of rules and goals is important to the end result. Consider the following examples:
 
   overlap(X,Y) :- member(M, X), member(M, Y).

   | ?- overlap(Z, [a, b, c, d]), member(Z, [1, 2, c, d]).
   (infinite computation)

   | ?- X = [1, 2, 3], member(a, X).
   no

   | ?- member(a, X), X = [1, 2, 3].
   (infinite computation)
The most interesting thing to note here is the difference between the answers of the second and third queries.

The second query simply answers "no", but the third query never returns. Why does this happen? The best way to examine what happens is to look at the associated relation tree which shows the structure of the possible solutions:

 
                      member(a,X)
                        /   \
    one ------>  X=[a|_]     X=[_|T]  <------ another
possibility                  ?member(a,T)         possibility
                               /   \              
                        T=[a|_]     T=[_|T']
                      X=[_,a,_]     ?member(a,T')
                                      /   \
                              T'=[a|_]     T'=[_|T'']
                           X=[_,_,a,_]     ?member(a,T''')
When we know X , as in the second query, we can compare a to each element in the list. No matches occur, so "no" is returned.

When we don't know X , as in the third query, the system keeps looking for elements in X to compare to a . . .it never finds any, but it keeps searching indefinitely.

In essence, it constructs the equivalent of a search tree and traverses it until if finds something that satisfies the goal.

Notice how the order of the expressions in the goal made a difference in how the result was deduced.


There is a design issue when considering the ordering of facts and rules. Traversing relation trees could be implemented either in a depth-first manner or a breadth-first manner.

Prolog uses depth-first "traversal" because each sub-goal must be satisfied before any subsequent sub-goals are satisfied. The ideal method would use breadth-first "traversal" where each sub-goal is examined in parallel.

However, breadth-first requires a large amount of memory and resources. This is why depth-first was used in Prolog. Unfortunately, this method does have its drawbacks as demonstrated by the following:

 
   f(X) :- f(X).                       ?f(1)
   f(1).                                / \ 
                                     f(1)  yes  <------ two
   | ?- f(1)                         / \                possibilities
   (infinite computation)         f(1)  yes
                                 / \
                                .
                               .
You might expect this to simply return f(1). In fact, if this were true logic, it SHOULD return f(1). However, because Prolog is implemented in a depth-first manner we never get to the second possibility on any level; we just keep traversing the rightmost branches. Thus, it traverses the tree forever.


Variables in terms

Variables in terms allow you to increase information using unification.
 
   | ?- L = [a, b | X].
   L = [a, b | X] ? ;
   no

   | ?- L = [a, b | X], X = [C, Y].
   L = [a, b, C, Y],
   X = [C, Y] ? ;
   no
Notice how the information about L is increased by unifying X to the list [C, Y] .

Unification of a variable representing the end of the list is similar to an assignment to that variable.


Backtracking

Backtracking simply refers to reconsidering sub-goals that were previously proven/satisfied. A new solution is found by beginning the search where the previous search for that sub-goal stopped.

Considering the possibility tree, backtracking refers to retracing you steps as you follow branches back up to previously untraversed branches.

A similar example to the example in the previous section:

 
   f(x) :- fail            ?f(1)
   f(1)                     / \
                        fail   yes
Steps followed:
  1. Trace from f(1) to fail.
  2. Trace back from fail to f(1). This is the backtracking step.
  3. Trace from f(1) to yes.