Copyright © 2008 David Schmidt

Chapter 7:
Introduction to Prolog


7.1 Databases and searches for witnesses
7.2 Horn clauses and backwards chaining
7.3 Data structures and unification
    7.3.1 List data structures
7.4 Example: Change Making
7.5 A few additional useful Prolog predicates
7.6 An implementation you can use
7.7 Conclusion


Prolog (``PROgramming in LOGic'') is a programming language based on the predicate calculus, restricted to ∀, —>, ∧ and a limited use of . The programs one writes are literally propositions from predicate logic, and the execution of the programs are literally proofs built using the ∀e, —>e, ∧i (and limited use of) ∃i laws.

Prolog was developed in the 1970s as an application of resolution theorem proving. Its ease of use, plus its inclusion of dynamic lists (like the ones in Python), make it a prominent (if not the prominent) language for solving problems in artificial intelligence, deductive databases, and data mining (let alone, theorem proving). A small amount of knowledge about Prolog goes a long way, and this chapter attempts to give you some knowledge.


7.1 Databases and searches for witnesses

A prolog program consists of a set of premises, the database. A user interacts with the database by supplies queries (goals to be proved). The Prolog interpreter uses its knowledge of deduction rules to prove or to refute the goals.

The simplest form of premise is a primitive proposition, like these two:

expensive(car).
expensive(house).
Read these as asserting ``a car is expensive''; ``a house is expensive.'' We can query the the database, like this:
?- expensive(car).
true

?- expensive(coffee).
false
The first query, ?- expensive(car), is immediately proved by the Prolog interpreter, and the second cannot be proved. (The interpreter examined all the premises in its database, and none matches the query.)

Prolog's power comes from this form of query:

?- expensive(X)
X = car.
This query asks, ``is there something, call it X, that is expensive?'' The interpreter replies with the value for X that makes the query proved true. More precisely stated, the query is really, ``∃X: expensive(X) ?'' The Prolog interpreter searched and found an individual (car) that proves the goal. (There is an implicit use of the ∃i-rule here.)

Prolog lets you repeat the same query, searching for multiple, different answers, by typing a semicolon, like this:

?- expensive(X).
X = car ;
X = house .
That is, the user typed, ?- expensive(X), the interpreter replied, X = car, the user typed ;, the interpreter replied, X = house, the user typed ;, the interpreter replied with a period, . (no more).

Here is a database that uses variables X and Y in its premises. (The variables are implicitly prefixed by ∀X and ∀Y in the premises.)

===================================================

likes(john, icecream).
likes(john, mary).
likes(X, kim).
likes(ed, Y).

===================================================
The first premise asserts that john likes icecream. The third premise can be read as everyone likes kim. It is more precisely understood as ∀X likes(X,kim). Variables (words that begin with capital letters) in premises are implicitly universally quantified. They are called logical variables, and they can be assigned values while the Prolog interpreter searches for a proof of a query.

Here are two queries (goals to be proved) for the previous database:

===================================================

?- likes(john,kim).
true

?- likes(X, icecream)
X = john ;
X = ed .

===================================================
The second query should be studied. The interpreter scanned the premises in the database, from top to bottom, in that order. (The order of the premises is crucial to Prolog programming.) The first match sets logical variable X=john. The second match, made with the fourth premise, sets X=ed and Y=icecream. The value of Y is not displayed to the user because it is an internal match and Y does not appear in the original query. Here is another query that does expose an internal logical variable:
?- likes(Z,kim)
true ;
Z = ed
Read the first answer, true, as saying that Z can be made equal to any value at all (all atoms, as they are called in Prolog. john, icecream, etc., are all atoms.) This answer was found with the third clause in the database, likes(X, kim). The second answer, Z = ed, used a different clause, namely, likes(ed, Y), to determine that ed likes kim.


7.2 Horn clauses and backwards chaining

We can write premises that show how to deduce new facts from the premises in the database. For example, we can deduce that X has Y as a sister if
(i) Y is female;
(ii) Y has father M and mother F;
(iii) X has the same father M and mother F.
We would write this law like this in predicate logic:
∀X ∀Y ∀M ∀W:
    female(Y)  ∧  parents(Y,M,W)  ∧  parents(X,M,W)
  —>  sisterOf(X,Y)
Here is how the law is written in Prolog:
sisterOf(X,Y) :- female(Y), parents(Y,M,W), parents(X,M,W).
That is, the four quantifiers, , are omitted; is written as a comma; and —> is written backwards, as :-. (That is, P —> Q is written Q :- P (read ``Q if P'').)

There is a key reason why the implication is written backwards. Say that we are asked to prove sisterOf(a,b) for some a and b. To do this, we must prove as subgoals all of female(b), parents(a,c,d), and parents(b,c,d), for some values c and d. So, the Prolog coding reads as a tactic for proving sisterOf (using —>e and ∀e).

Here is the new law we wrote plus a database of primitive propositions:

===================================================

sisterOf(X,Y) :- female(Y), parents(Y,M,W), parents(X,M,W).

parents(bart, homer, marge).
parents(lisa, homer, marge).
parents(maggie, homer, marge).
female(marge).
female(lisa).
female(maggie).

===================================================
Here is a query:
===================================================

?- sisterOf(bart, Z)
Z = lisa ;
Z = maggie .

===================================================
This shows bart has two sisters.

Here is a summary of the steps taken by the Prolog interpreter to prove Z=lisa:

  1. The goal is sisterOf(bart, Z). This matches the first premise, the implication, where X=bart and Z=Y. Three new subgoals are generated: female(Y), parents(Y,M,W), and parents(X,M,W). The goals will be solved in that order. So far, the variables are assigned as
    Z = Y
    X = bart
    

  2. Subgoal female(Y) is proved by the fifth premise, where Y=marge. We have these variables:
    Z = Y
    X = bart
    Y = marge
    

  3. Now, parents(Y,M,W) is tackled. Since Y=marge, a match must be found for parents(marge,M,W). None is found --- the subgoal fails. To repair the failure, the Prolog interpreter backtracks to the previous subgoal, female(Y), and tries to prove it with a different value for Y:
    Z = Y
    X = bart
    Y = ?
    

    Restarting the earlier step, the sixth line of the database matches female(Y), for Y=lisa. We have these variables and their values:

    Z = Y
    X = bart
    Y = lisa
    

  4. Next, parents(Y,M,W) is reconsidered, where Y=lisa. This is proved for M=homer and F=marge:
    Z = Y
    X = bart
    Y = lisa
    M = homer
    F = marge
    

  5. The last goal to prove is parents(X,M,W), and given the above values of the logical variables, this is proved by line 2 of the database.

    There are no more subgoals to prove, so the original query is proved. Z=lisa prints.

When the query is repeated, the Prolog interpreter resumes its proof search, acting as if Y=lisa had failed, and it searches for yet another, different, solution, working from the goal, female(Y):
Z = Y
X = bart
Y = ?
M = ?
F = ?
The search sets Y=maggie, and the last two goals are reproved, with M=homer and F=marge.

You can ask the Prolog interpreter to generate an execution trace of exactly these searching steps. The way to do so is to trace all the predicate names in which you have interest. In the above example, we would type:

?- trace(sisterOf).
?- trace(parents).
?- trace(female).
Then, when we enter the query,
sisterOf(bart, Z).
We see a printout of the clauses used by the interpreter while it searches for a value for Z:
===================================================

[debug] 9 ?- sisterOf(bart,Z).
 T Call: (7) sisterOf(bart, _G464)
 T Call: (8) female(_G464)
 T Exit: (8) female(marge)
 T Call: (8) parents(marge, _L174, _L175)
 T Fail: (8) parents(marge, _L174, _L175)
 T Redo: (8) female(_G464)
 T Exit: (8) female(lisa)
 T Call: (8) parents(lisa, _L174, _L175)
 T Exit: (8) parents(lisa, homer, marge)
 T Call: (8) parents(bart, homer, marge)
 T Exit: (8) parents(bart, homer, marge)
 T Exit: (7) sisterOf(bart, lisa)
Z = lisa 

===================================================
The trace shows us that the interpreter first tried to use marge as a value for Z, bart's sister, but this failed because the bart and marge cannot be proved to have the same parents. Notice also that the intepreter invented its own internal variables, _G464, _L174, and _L175, while it did its search. These variables are not revealed to the human user.

When we continue the trace by typing a semicolong, we see

===================================================

Z = lisa ;
 T Redo: (8) female(_G464)
 T Exit: (8) female(maggie)
 T Call: (8) parents(maggie, _L174, _L175)
 T Exit: (8) parents(maggie, homer, marge)
 T Call: (8) parents(bart, homer, marge)
 T Exit: (8) parents(bart, homer, marge)
 T Exit: (7) sisterOf(bart, maggie)
Z = maggie.

===================================================
Which shows how the interpreter continues as if it failed to find a value for Z the first time.

If we would write the proof for Y=lisa in the laws of predicate logic, it would look like this:

===================================================

Database |− ∃X sisterOf(bart, X)

1. ∀X ∀Y ∀M ∀W
  (female(Y)  ∧  parents(Y,M,W)  ∧  parents(X,M,W))
       —>  sisterOf(X,Y)                                premise
2. female(lisa)                                           premise
3. parents(lisa, homer, marge)                            premise
4. parents(bart, homer, marge)                            premise
5. female(lisa)  ∧  parents(lisa, homer, marge)
       ∧ parents(bart, homer, marge)                     ∧i (2 times)
6. (female(lisa)  ∧  parents(lisa,homer,marge) 
        ∧  parents(bart,homer,marge))
   —>  sisterOf(bart,lisa)                            ∀e (4 times)
7. sisterOf(bart,lisa)                                    —>e 6,5
8. ∃Y sisterOf(bart,Y)                                 ∃i 7

===================================================
The key is finding the appropriate atoms for the variables, X, Y, M, and F. Prolog does this by exhaustive search of the database, backtracking as needed until a useful combination is found.

The backwards implication is called a Horn clause, and the Prolog's use of backwards implications to generate subgoals that are eventually all proved is called backwards chaining.


7.3 Data structures and unification

Prolog can handle data structures that are based on logical function symbols. For example, in arithmetic, when we write 3+2, the + is a function symbol. Prolog lets us invent function symbols useful to our subject area.

For example, say that we work in a library and we will program a Prolog database of the library's holdings. Perhaps the library stocks books and dvds, so the items can be portrayed as data structure-values like these:

book('David Copperfied', 'Charles Dickens')

dvd('Tale of Two Cities')
The first is a book data structure that holds two data values: the book's title and its author. (Prolog allows strings to be atoms.) The second data structure is a dvd structure that holds the dvd's title. The data-structure-builder names, book and dvd, are called functors because they look like function calls (but they are not).

If the library owns a copy of each of these items, we can write a predicate that asserts these facts:

owns(k0, book('David Copperfied', 'Charles Dickens')).

owns(k3, dvd('Tale of Two Cities')).
The predicate, owns, lists the key (id number) and the item. So, the library owns item k0, the book David Copperfield by Charles Dickens. Here is a summary of the data structures we will use to define the library's database:
ITEM ::=  book(TITLE, AUTHOR)  |  dvd(TITLE)
                where TITLE and AUTHOR are strings
We will use these two predicates:
PRED ::=  owns(KEY, ITEM)  |  borrowed(KEY, PERSON, DATE)
                where KEY is an atom that begins with  k
                      ITEM is defined above
                      PERSON is a string
                      DATE is an int
The borrowed predicate remembers who has borrowed items from the library. Here is a sample database:
===================================================

owns(k0, book('David Copperfied', 'Charles Dickens')).
owns(k1, book('Tale of Two Cities', 'Charles Dickens')).
owns(k2, book('Tale of Two Cities', 'Charles Dickens')).
owns(k3, dvd('Tale of Two Cities')).
owns(k4, book('Moby Dick', 'Herman Melville')).

borrowed(k2, 'Homer', 44).
borrowed(k4, 'Homer', 46).
borrowed(k3, 'Lisa', 92).
borrowed(k0, 'Lisa', 92).

===================================================
Whenever someone borrows an item, the appropriate borrowed premise is added to the database. When someone returns an item, the borrowed premise is removed. (Prolog has special operators, asserta and retract, for adding and removing premises from an active database.)

Given the above database, here are some queries. First, what has Homer borrowed?

?- borrowed(K, 'Homer', _).
K = k2 ;
K = k4 ;
false.
The database is written so that the items' keys are retrieved. We can use them: What books has Homer borrowed?
?- borrowed(K, 'Homer', _), owns(K, book(_,_)).
K = k2 ;
K = k4 ;
false.
The above example is important --- it shows how to retrieve a key of a book (K) and how to use the key as well as the data-structure name (functor) to match only the books borrowed by Homer. The action of matching functor names to the database is called unification, and it gives much power to Prolog. Note also that the _ symbol is a ``dummy variable'' that we do not care about --- its value is not printed. (Here we do not care about the book's due date nor the title and authors, so three dummy variables are used in the query.)

Of course, if we wished to see the titles, we modify the query like this:

?- borrowed(K, 'Homer', _), owns(K, book(T,_)).
K = k2,
T = 'Tale of Two Cities' ;
K = k4,
T = 'Moby Dick' ;
false.

Libraries want to track overdue books. Here is a law for proving when a borrowed item is overdue:

===================================================

isOverdue(Person, Item, Today) :- borrowed(Item, Person, DueDate),  Today > DueDate.

===================================================
The words. Person, Item, and Today are logical variables, because they begin with capital letters. Today is an int. (See our definitions earlier.) This law uses arithmetic on ints: it compares the int-value of Today with the int value of DueDate, to see if today is greater than the due date. If so, the goal, Today > DueDate succeeds (is proved).
?- isOverdue('Homer', _, 89).
true ;
true ;
false.

?- isOverdue('Homer', Item, 89).
Item = k2 ;
Item = k4 ;
false.
We query the database and learn that, if today is day 89, then Homer has two items overdue; we can learn the keys of the items if we wish.

Perhaps a fine is calculated by how many days an item is overdue. We can use simple arithmetic in Prolog, like this:

===================================================

hasFine(Person, Item, Today, HowMuch) :- isOverdue(Person, Item, Today),  
                                         borrowed(Item, _, DueDate),
                                         HowMuch is  Today - DueDate.

===================================================
The predicate, HowMuch is Today - DueDate is an assignment --- a matching --- of HowMuch to the answer computed from Today - DueDate. This ``subgoal'' always succeeds.
?- hasFine('Homer', I, 89, Fine).
duedate=44
I = k2,
Fine = 45 ;
duedate=46
I = k4,
Fine = 43 ;
false.
You can also do simple arithmetic with +,*,-,/.

Finally, it would be helpful to write a query that would return a list of all the items that Homer has borrowed. To do this, we must learn how to use lists in Prolog.


7.3.1 List data structures

Prolog's lists are just like Python's lists; they are written like this:
[a, b, c]

[2,4,6,8]

['Homer', book('Tale of Two Cities', 'Charles Dickens')), book('Moby Dick', 'Herman Melville')) ] 

['Homer', [ book('Tale of Two Cities', 'Charles Dickens')),
            book('Moby Dick', 'Herman Melville')) ] ]

[]
As in Python, lists can be a mixture of values, and lists can be placed within lists. The list builder, [_], is a functor, just like book(_) and dvd(_), seen in the previous section.

Unlike Python, Prolog does not use ints to index individual elements from a list. Instead, you can index the front (``head'') element of a list, and you can index the rest (``tail'') of the list without its front element. This head-tail indexing is done with a pattern matching that takes a little practire. The pattern looks like this: [H|T].

If we match this pattern against [a,b,c], we have H=a, T=[b,c].

If we match this pattern against ['Homer', book('Tale of Two Cities', 'Charles Dickens')), book('Moby Dick', 'Herman Melville')) ] we have H = 'Homer', T = [ book('Tale of Two Cities', 'Charles Dickens')), book('Moby Dick', 'Herman Melville')) ] .

If we match this pattern against ['Homer', [ book('Tale of Two Cities', 'Charles Dickens')), book('Moby Dick', 'Herman Melville')) ] ], we have H = 'Homer', T = [[ book('Tale of Two Cities', 'Charles Dickens')), book('Moby Dick', 'Herman Melville')) ] ].

If we try to match this pattern against [], it fails --- there is no match.

We use the [H|T] pattern to write ``functions'' that compute on lists. Here is a function that looks at a list (the first argument) and returns the head of the list as its answer (the second argunent):

getFirst([H|T], H) :- .
This law has no subgoals. When we use it in Prolog, we type just this (not like above):
getFirst([H|T], H).
We can use the ``function'' like this:
?- getFirst([a,b,c], Ans).
Ans = a.
    
?- getFirst([], Ans).
false.
The first query asks if there is a value for Ans that ``proves'' getFirst([a,b,c], Ans). The law, getFirst([H|T], H), says we merely set the second argument equal to the head of the first argument and the Prolog interpreter proves the goal with this assignment to the logical variables:
Ans=H
H=a
T=[b, c]

Here is another example:

===================================================

getRest([H|T], T).

?- getRest([a,b,c], Ans).
Ans = [b,c].

?- getRest([], Ans).
false.

===================================================
Here is a third example, which extracts the second element in a list (that has at least two elements):
===================================================

getSecond([A|[B|C]], B).

?- getSecond([a,b,c], Ans).
Ans = b.

===================================================
These small examples show that there is a style to writing predicates in Prolog so that they behave like functions. In Python, we write functions in this style:
def f(x,y) :
    . . .
    return ans
The parameters, x and y are listed separately from the ans. But in Prolog, we write,
f(x, y, ans) :- . . .
where the ans variable is written next to the parameters. A value is ``assigned'' to ans by Prolog's matching/unification operations that are used when the Prolog interpreter tries to solve a goal.

This style of writing a function as a predicate with parameters and answer variable grouped together is called relational programming.

We can make Prolog-style functions powerful with recursive references. In Python, we might search a list a for an element, v, like this, using recursive calls:

def member(v, a):
    if len(a) > 0 :              # is list  a  nonempty ?
        H = a[0]
        if H == v :              # is  v  at head of  a ?
           return True
        else :
           T = a[1:]
           return  member(v, T)  # if not, search inside tail of  a
    else : 
        return False             # list  a  is  []
We write this same recursive search in Prolog like this: (Important: the order of the clauses is crucial to the success of the proof search!)
===================================================

member(V, [V|_]).
member(V, [_|T]) :- member(V, T).

===================================================
We test the function with this example:
?- member(c, [a,b,c]).
Here's what happens:
  1. To prove the goal, the interpreter tries to match it against member(V, [V|_]). This fails, because atom c is not the same as atom a within list [a,b,c].

    The next law is tried, matching against member(V, [_|T]). This match succeeds. The new goal is member(V,T), where

    V=c
    T=[b,c]
    

  2. To prove goal member(V,T), that is, member(c,[b,c]), a match is attempted against member(V, [V|_]). This fails.

    Next, a match of member(c,[b,c]) again member(V, [_|T]) against member(c,[b,c]) is attempted. This succeeds. Since this premise was used once before, its logical variables, V and T, were used once before, so new variants, V1 and T1, are invented. The assignments to the logical variables look like this:

    V=c
    T=[b,c]
    V1=V
    T1=[c]
    
    and the new goal is member(V1,T1), that is, member(c,[c]). Remember: if a premise is used more than once to build a proof, new instances of its logical variables are created for each successful match. (Of course, this is because there is an implicit use of in the premise, and we are using the premise multiple times with the e rule!)
  3. Finally, the goal, member(c,[c]), matches against the first premise, and H=c. The proof is completed:

    V=c
    T=[b,c]
    V1=V
    T1=[c]
    H=V1
    
    ?- member(c, [a,b,c]).
    yes
    

If we ask the Prolog interpreter to trace the query,

?- trace(member).
we see this:
[debug] 3 ?- member(c, [a,b,c]).
 T Call: (7) member(c, [a, b, c])
 T Call: (8) member(c, [b, c])
 T Call: (9) member(c, [c])
 T Exit: (9) member(c, [c])
 T Exit: (8) member(c, [b, c])
 T Exit: (7) member(c, [a, b, c])
true 

In a similar way, we can write a Prolog function to see if a value is not a member of a list: (Here, \= means !=.)

===================================================

notMember(V, []).
notMember(V, [H|T]) :- V \= H,  notmember(V, T).

===================================================
This two-line function computes the same steps as this Python function:
def notMember(v, a):
    if a == [] :
        return True
    else :
        if v != a[0] :
           return  notMember(v, a[1:])  #search tail of  a
        else :
           return False                 # v == a[0], so stop

Here is a third example, where we write a function that accepts a list of ints and constructs a list that has all the ints doubled in value. It will behave like this:

?- double([1,2,3], Y).
Y = [2,4,6].

?- double([], Y).
Y = [].
Here is the function:
===================================================

double([], []).
double([H|T], [HH|TT]) :-  HH is 2 * H,  double(T, TT).

===================================================
The computation made by double looks like this in Python:
def double(a) :
    if  a == [] :
        return []
    else :
        HH = a[0]
        TT = double(a[1:])
        return [HH] + TT

We can now finish the library-database example from the previous section. How do we compute a list of all the items borrowed by a user? Prolog has a built-in predicate, findall, that does this. Here is how to define the list of books borrowed by 'Homer':

?- findall(Item, borrowed(Item, 'Homer', DueDate), Ans).
Ans = [k2, k4].
The predicate, findall(WHAT, QUERY, ANSWERLIST), repeats the QUERY over and over, collecting each value of variable WHAT computed by the QUERY, saving the values in the list, ANSWERLIST.

You can use findall to save multiple values, say the Item and DueDate of each book borrowed:

?- findall([Item,DueDate], borrowed(Item, 'Homer', DueDate), Ans).
Ans = [[k2, 44], [k4, 46]].

If you understood this example, then you can do these exercises:


7.4 Example: Change Making

Prolog is useful for performing exhaustive search --- a skill valuable in data mining and artificial intelligence. Here is a small example that exploits this. It is from http://www.csupomona.edu/~jrfisher/www/prolog_tutorial:

This Prolog program checks and generates change adding up to a dollar consisting of quarters, dimes, nickels, and pennies.

===================================================

change([Q,D,N,P]) :- 
        member(Q,[0,1,2,3,4]),                  /* quarters     */ 
        member(D,[0,1,2,3,4,5,6,7,8,9,10]) ,    /* dimes        */ 
        member(N,[0,1,2,3,4,5,6,7,8,9,10,       /* nickels      */ 
                   11,12,13,14,15,16,17,18,19,20]),  
        S is 25*Q +10*D + 5*N, 
        S =< 100, 
        P is 100-S. 

===================================================
Examples:
?- change([Q,D,N,P]). 
lists all possible ways of giving change for a dollar. We can also check if a proposed quantity of change totals a dollar:
?- change([2,3,4,6]). 
no 
since 2 quarters, 3 dimes, 4 nickels, and 6 pennies do not make a dollar, and
?- change([2,3,2,P]). 
P=10 
calculates how many pennies are needed to make all the coins total a dollar.


7.5 A few additional useful Prolog predicates

The simplest way to use Prolog is to type the database clauses into a file, named, say, myfile.pl. Start the file by double-clicking on its icon --- you will receive a command window into which you can type queries. If you wish to update the clauses in myfile.pl, edit it, stop the command window, and restart it.

In addition to the commands shown in the previous examples, here are a few more.


7.6 An implementation you can use

You can download a free copy of SWI-Prolog at www.swi-prolog.org. Once you install it, the simplest way to use it is to use a text editor to type a file that holds your Prolog database. Name the file with the extension, .pl (e.g., myProgram.pl. When you double-click on the icon for your file, this starts the Prolog interpreter, which loads the contents of your file into its internal database. A command window next appears, into which you can type queries. When you must modify your program, close the command window, edit your program, and restart it.


7.7 Conclusion

We have only begun to explore the power that comes from modelling computation within predicate logic. With the addition of lists and recursively defined predicates, one has enough computational power such that any problem whose solution can be written in a programming language can also be solved in Prolog.