Chapter 5 of our text introduces programming in Prolog (although not in a very complete manner).
A very (very) basic summary of Prolog syntax can be found courtesy of Dr. Ralph Morelli at Trinity College.
A very nice intermediate introduction to prolog, in the form of lecture notes, is provided by Ulle Endriss of Universiteit van Amsterdam.
A wonderful Prolog tutorial, with more in-depth exercises and examples on which we will rely, is found courtesy of Dr. John Fisher at CalState Pomona
If you want to challenge yourself, check out the following Ninety-Nine Prolog Problems (with solutions embedded).
We will be relying on the SWI-Prolog implementation. (As an aside, there will be a Boot Camp in SWI-Prolog happening as a preconference workshop on September 18th, as part of this year's StrangeLoop conference in UCity.)
SWI-Prolog is installed on turing (as an executable swipl), and it is freely available for all computing platforms. We recommend that you use SWI-PL for consistency with this course, since not all Prolog implementations use precisely the same commands.
Prolog source code is typically saved in a file using the .pl extension (although some use .pro). You can use your favorite editor, or there is a GUI IDE included with SWI-Prolog that can be invoked from the Prolog shell as prolog_ide.
tracing/debuging There is also advanced support for tracing and debugging prolog programs, that will be very instructive in demonstrating the effective flow of control of the program. This can be done by executing the command trace. at the prolog query prompt, and if you first execute guitracer, you will get the graphical version of a trace (rather than the default terminal output). For maximum info, we suggest the following combination:
guitracer. visible(+all). trace.
Facts and Rules (see below), are typically saved in advanced within a .pl file. For example, assume we have a file likes.pl that contains some definitions. That script can be executed from the command line using the command
swipl -s likes.plOtherwise, you can start up the swipl program, and then load the script using a command
[likes].within the interpreter, in which case it will look for a file likes.pl in the working directory. You may also explicitly give the full file name within single quotes, as
['likes.pl'].That is sometimes necessary if the filename has unusual characters, such as underscores.
As our first example, we define a two-parameter predicate likes, such that likes(A,B) designates that A likes B.
We might populate a world with the following facts, stored in a file
friends.pl (This file is also provided within
likes(ross,rachel). likes(rachel,ross). likes(gunther,rachel). likes(janice,joey). likes(janice,chandler). likes(chandler,monica). likes(monica,chandler). likes(ross,charlie). likes(joey,charlie). likes(joey,rachel). likes(rachel,joey). likes(charlie,ross).
Note that constants and predicates must start with a lowercase letter in Prolog.
We can then execute simple queries, such as:
?- likes(rachel,ross). true . ?- likes(rachel,gunther). false.
We may also use a variable, which must start with an uppercase letter in Prolog, as part of a query, as follows.
Prolog attempts to unify the variable X in a way that makes the query true. Furthermore, it uses backtracking to try to find all possible solutions. If you try this, it will present you with the first solution and then pause. If you press space (or ;), it will present you with another, and so on. However if you enter a period (.) after seeing a solution, it ends the query without showing you any further solutions.?- likes(X,rachel). X = ross ; X = gunther ; X = joey.
You may use multiple variables, and express a conjunction of several facts by giving a comma-separated set of goals. For example, we can query for pairs of people who like each other, as:
Note well that each pair of compatible couples is listed twice as a solution, due to the symmetry of X and Y (e.g, X=ross, Y=rachel, as well as Y=rachel, X=ross).?- likes(X,Y), likes(Y,X). X = ross, Y = rachel ; X = rachel, Y = ross ; X = chandler, Y = monica ; X = monica, Y = chandler ; X = ross, Y = charlie ; X = joey, Y = rachel ; X = rachel, Y = joey ; X = charlie, Y = ross.
We can create new predicates that depend on this one by defining one or more rules. For example, we can define a compatible predicate adding the following rule.
compatible(X,Y) :- likes(X,Y),likes(Y,X).This is really Prolog's syntax for a Horn clause, with compatible(X,Y) being the head of the clause. In PL1 syntax, we have stated that
∀X ∀Y likes(X,Y) ∧ likes(Y,X) ⇒ compatible(X,Y)Here is another example of a predicate:
This is equivalent to the PL1 statementis_liked(Y) :- likes(X,Y).
∀Y (∃X likes(X,Y)) ⇒ is_liked(Y)
To develop more complex rules, we will use a knowledge base involving parents and children. To allow for some interesting relationships such as half-siblings, I decided why not look to Hollywood. I've created a knowledge base hollywood.pl (available on turing) that includes portions of the family tree for the extended Sheen family (Martin, Charlie, ...) and the Douglas family (Kirk, Michael).
It includes three types of base predicates, including:
male(michael_douglas); male(kirk_douglas); parent_of(kirk_douglas, michael_douglas). female(catherine_zeta_jones). parent_of(catherine_zeta_jones, dylan_douglas). ...
We will work together to define various new predicates based on this. Interesting relationships might include:
mother_of(A,B) father_of(A,B) grandparent_of(A,B) siblings(A,B) halfsiblings(A,B) cousins(A,B) descendant_of(A,B) related(A,B)
Constants, functions, and predicates must being with lowercase letter. (e.g., ross, mother_of)
Variables should begin with an uppercase letter (e.g., X) Constants, functions, and predicates must being with lowercase letter.
There is a special variable, _, that can be used when we have no other reason to refer to the unified object in any other expression (and we do not care to see the unified result within the output).
Fact
Simplest rule is a fact, which would be stated as
likes(ross,rachel).ending with a period.
Implication
A more general rule states how a goal can be satisfied by
meeting some other subgoal.
liked_by(X,Y) :- likes(Y,X).In this case, we are stating that if likes(Y,X) is true, then we can infer liked_by(X,Y). In PL1 syntax, we have stated that
∀X ∀Y likes(Y,X) ⇒ liked_by(X,Y)Prolog typically treats a rule as a Horn clause, where the goal is known as the head of the rule (e.g., liked_by(X,Y) in this case), and the rest of the rule is the body. The head must be an atomic formula. The body can be a more complex formula.
Conjunction
Prolog uses the comma symbol (,) to represent the "and" operator,
either as part of a query, or as part of the body
of a rule. So we can query for someone who is liked by both Ross
and Joey, using syntax:
?- likes(ross,X), likes(joey,X).Note that the variable X must be unified to the same value for this conjunctive query to be satisified. (However, if using the wildcard _ variable, each use is independent).
A conjunction is often used as part of a body of a rule, to express Horn clauses. We say this in the earlier example:
compatible(X,Y) :- likes(X,Y),likes(Y,X).This is really Prolog's syntax for a Horn clause, with compatible(X,Y) being the head of the clause. In PL1 syntax, we have stated that
∀X ∀Y likes(X,Y) ∧ likes(Y,X) ⇒ compatible(X,Y)
Disjunction
Technically, Prolog allows for a disjunction to be expressed
using the semicolon character (;) for the "or" operator.
tension(X,Y) :- likes(X,Y); likes(Y,X).which could be stated in PL1 as
∀X ∀Y likes(X,Y) ∨ likes(Y,X) ⇒ tension(X,Y)In apply this rule, Prolog will first try to prove the first subgoal. Only if it is unable will it attempt to satisfy the second subgoal. This disjunction can be expressed instead using two separate traditional rules:
As before, it will attempt to use the first rule to satisfy the goal tension(X,Y), and will only use the second rule if the first could not be satisfied.tension(X,Y) :- likes(X,Y). tension(X,Y) :- likes(Y,X).
Negation
Prolog has a predicate not(...) that can be used to
express negation. For example, we can query
?- not(likes(rachel,ross)).which will be true if it is unable to satisfy likes(rachel,ross).
This can be used in the body of a rule as well, such as
spurns(X,Y) :- not(likes(X,Y)).
However, when there is a free variable within an expression, such as not(p(X)), that is equivalent to the PL1 expression
∀X ¬p(X)which we know is equivalent to
¬ ∃X p(X)Therefore the expression not(p(X)) is only true if there does not exist any instantiation for X satisfying p(X).
Therefore, you cannot use the not syntax to enumerate through unsatisfying assignments. For example the query
?- not(likes(X, rachel)).does not find all X such that X does not like rachel. This query will simply return false.
Note: There is also a prefix operator (\+) for expressing negation in Prolog. Thus the expression
\+ p(X)is the same as
not(p(X))
Recall that predicates in PL1 always evaluate to true or false. Functions serve a different syntactic purpose as a term that can be used as an argument to another function or predicate.
In Prolog, functions must start with a lowercase letter, and they are often used to represent more structured data. For example, we might write a fact
owns(michael, book(programming_in_prolog, mellish)).In this case, owns is taking two arguments: the first is michael and the second is the term book(programming_in_prolog, melish). In this case, book represents a function with two arguments. Those arguments are typically structured by the programmer, for example so that the first is the title and the second is the author.
The purpose of such a function is that we could write other rules that might only be unified with terms that are also books, such as:
wrote(Y, book(X,Y)).which is true if Y is the author of the given book.
When two different variable names are used, this does not meant that they get unified to two different results. That is X and Y in an expression, unless otherwise stipulated, could be bound to the same or to different values. We can force a more specific unification using the following operators:
| A = B | A and B are unified to the same term |
| A \= B | A and B are unified to different terms |
| A @< B | A is unified to a term that is canonically "before" B. |
?- compatible(X,Y).nominally reports 8 satisfying assignments:
?- compatible(X,Y), X @< Y.
Note well that we would not want to put such a
symmetry breaking condition in the original rule that defined
the compatible predicate, because we want to consider
either of
Arithmetic operators
include +, -, *, and /,
using infix notation such as
| A + B | addition |
| A - B | subtraction |
| A * B | multiplication |
| A / B | division |
| A mod B | modulus |
| abs(A) | absolute value |
| max(A,B) | maximum |
| min(A,B) | minimum |
The result of a computation is assigned to a variable using the keyword is, as in,
(this is not to be confused with the = operator that is used to state that two variables must be unified to the same thing.)A is X + Y
Important Note: The is operator may only be applied when all variables in the righthand expression are already bound to appropriate values. You can check if a variable is currently unbound by using the built-in predicate var(X), which is true if X is unbound. Alternatively, you can test if a variable is bound to a numeric type using predicate number(X), which will be true only if X is already bound to a number.
Comparison operators resulting in booleans are:
| A > B | greater than |
| A >= B | greater than or equal to |
| A < B | less than |
|
A =< B
|
less than or equal to
(note well the placement of =) |
| A =\= B | not equal to |
| A =:= B | equal to |
Example of factorial (from Section 2.2 of Fisher tutorial)
The middle argument is being used as an accumulator to effectively store intermediate computations. We can compute a factorial of a number with this formula with a query such as:factorial(0,F,F). factorial(N,A,F) :- N > 0, A1 is N*A, N1 is N-1, factorial(N1,A1,F).
(try tracing this call)?- factorial(10,1,Answer).
To allow much greater functionality, Prolog goes well beyond the traditional PL1 semantics, supporting efficient processing of lists as built-in data type. (Lists are the core data representation in LISP, one of the earlier languages used for AI).
syntax
The expression [] is the empty list.
The expression [X|Y] is a nonempty list
having X as its first element and Y as a
(possibly empty) list representing the rest of the elements.
More generally, an expression such as [A, B, C | R] can be unified with any list that has three or more elements, in which case A, B, and C will be the first three elements respectively, and R will be a (possibly empty) list representing any remaining elements.
recursion
With lists comes the prominence of recursion as a
succinct representation of computation.
member
There is a builtin predicate, member(X,L) that is
true if X is an element of list L.
The definition of the member function can be expressed as
follows:
member(X,[X|_]). /* X is a member of a list if X starts that list */ member(X,[_|R]) :- member(X,R). /* X is a member of a list if it is in the rest of the list */
append
This is also a built-in predicate, such that
append(X,Y,Z)
is true if Z is a list that is the concatenation of lists X
and Y. For example,
Although built-in, a recursive definition could have been written as:
apppend([],L,L). /* A base case */ append([X|R],S,[X|T]) :- append(R,S,T). /* A recursive case with nonempty first list */
reverse
Two version: an inefficient two-argument version that relies
on append, and an efficient three-parameter version that uses
an accumulator.
(see Eretl for details)
strip
We want a predicate such that
strip(X,L,M) if list M
is equal to list L with all occurrences of element X removed
(if any).
(implementation left as exercise)
prefix
We want a predicate such that
prefix(L,M) is true if list L
is a (not necessarily proper) prefix of list M.
For example
(implementation left as exercise)
nested list as a tree representation
list as an accumulator
We have seen that for some interactive queries, there may be several satisfying bindings. For example, we earlier offered the example:
A user can interactively step through all such bindings for X. However, it is useful for many tasks to be able to gather all of those bindings programmaticly, so that they can be used as part of a larger computation.?- likes(X,rachel). X = ross ; X = gunther ; X = joey.
Prolog offers three very useful built-in predicates that let you gather solutions to a query into a list. They are:
As an example, we could build a list of all people who like rachel as:
?- findall(X, likes(X,rachel), L). L = [ross, gunther, joey].
The findall and bagof predicates differ only
in the way they handle free variables that appear in
the goal statement (in our first example, there were no free
variables). For example, we might be interested in what X can be
used to satisfy
?- findall(X, likes(X,Y), L). L = [ross, rachel, gunther, janice, janice, chandler, monica, ross, joey|...].
When using bagof, it treats Y as an unbound variable, and is now finding combinations of Y and L that make the statement tree. The outcome appears as follows:
?- bagof(X, likes(X,Y), L). Y = chandler, L = [janice, monica] ; Y = charlie, L = [ross, joey] ; Y = joey, L = [janice, rachel] ; Y = monica, L = [chandler] ; Y = rachel, L = [ross, gunther, joey] ; Y = ross, L = [rachel, charlie].
The setof predicate also ensures that the result is sorted, and that duplicates are removed (duplicates could reflect that there were several rules that could be used to validate a particular binding).
For more on this topic, see the corresponding section in Dr. Fisher's tutorial .
You can intentionally generate formatted output in Prolog by placing certain commands as clauses in a rule that will only be applied if earlier subgoals have been met.
The two commands we will see are:
(more details needed...)
Example:
max(X,Y,X) :- X >= Y, !. /* Once we know that X >= Y, do not consider any other unification */ max(X,Y,Y). /* So this rule is only reached when Y > X. */
Example:
member(X,[X|_]) :- !. /* if the first rule succeeds, never backtrack to the second rule */ member(X,[_|R]) :- member(X,R).