Elements of Prolog Programs
Syntax
All Prolog statements (axioms, rules, and goals) are Horn clauses
except they are written with implicit quantification of variables and "backwards" as
Q0 :- Q1, Q2, …, Qn.
The different parts of a Horn clause are given distinct names:
-
Q0
is the head -
Q1, Q2, …, Qn
is the body
Each Qi
is a Prolog term, where a term is:
-
an atom (a name starting with a lower-case letter):
fluet
,csci_344
, `… -
a number:
1
,2
, … -
a structure, which is:
-
a functor (a name starting with lower-case letter)
-
applied to a parenthesized list of terms:
teaches(fluet, csci_344)
, …
-
-
a logical variable (a name starting with an upper-case letter):
Professor
,Class
, …
A Prolog term with no variables is called a ground term.
Note that atoms and structures play two roles in Prolog:
-
they can be used to express predicates, where an atom corresponds to a nullary predicate (i.e., a predicate of no arguments) and a structure corresponds to a non-nullary predicate (i.e., a predicate of one or more arguments).
-
they can be used to express data structures, where an atom corresponds to atomic data and a structure corresponds to structured data.
All Prolog statements (axioms, rules, and goals) are Horn clauses:
-
axiom: a statement with a head (and no body)
Q0.
Meaning:
Q0
is assumed to be true. -
rule: a statement with a head and a body
Q0 :- Q1, Q2, …, Qn.
Meaning:
Q1
,Q2
, …, andQn
are provable, thenQ0
is provable. -
goal: a body (with no head)
?- Q1, Q2, …, Qn.
Meaning: Attempt to prove
Q1
,Q2
, …, andQn
.
Execution
Recall that, broadly speaking, logic programming is comprised of two steps:
-
Construct a collection of axioms and rules and pose a goal
-
Axioms: logical statements assumed to be true (i.e., assumed to be a fact)
-
Rules: logical statements that derive new facts (from existing facts)
-
Goal: logical statement to be proven true (or disproven (i.e., proven false))
-
-
System attempts to prove goal from axioms and rules
Prolog uses Horn clauses in conjunctive normal form (a restriction of first-order predicate logic) for axioms, rules, and goals and uses backward-chaining with depth-first search for proof search.
Ultimately, Prolog must determine if a goal is true (technical, "can be proven with respect to the given axioms and rules"). Typically, a goal in Prolog includes variables; for example:
?- sort([2,5,3,9,7,1,4,0,8,6],L).
Remember that a goal is a body (without a head) and variables that only occur in the body are implicitly existentially quantified. Thus, the above corresponds to the logical statement:
∃ L. sort([2,5,3,9,7,1,4,0,8,6],L).
In order to demonstrate that this statement is provable, Prolog must find a
witness for the variable L
. An essential aspect of Prolog’s execution is
determining the instantiations of variables that occur in goals, which is
accomplished through the use of unification.
Unification
The process of unification determines whether or not two Prolog terms "match", meaning that there is an instantiation of the variables in the two terms that makes the two terms identical. The result of a successful unification is a most general substitution --- an environment mapping variables to terms that makes the two terms identical and is less restrictive than any other substitation that makes the two terms identical.
Informally, unification proceeds by comparing the abstract syntax of the two terms:
-
Two atoms unify iff they are the same atom.
-
Two numbers unify iff they are the same number.
-
Two structures unify iff they have the same functor, the same number of arguments, and corresponding arguments recursively unify.
-
A variable unifies with (almost) anything, yielding a substitution.
The one exception to a variable unifying with anything is that a variable cannot unify with non-variable term that includes the variable itself. This is Prolog’s occurs check; see example below.
Examples:
Term | Term | Unification |
---|---|---|
|
|
unify with |
|
|
do not unify |
|
|
unify with |
|
|
unify with |
|
|
unify with |
|
|
unify with |
|
|
unify with Note that |
|
|
unify with |
|
|
do not unify |
|
|
unify with |
|
|
unify with |
|
|
unify with Note that |
|
|
does not unify Note that When performing unification of a variable and a non-variable term, Prolog checks whether or not the variable occurs in the term; if it does, then the unification fails. This is Prolog’s occurs check. |
Satisfiability and Proof Search
We can now more precisely define what it means in Prolog for a goal to be true
("to be provable with respect to the given axioms and rules"). A goal is true
if each term is satisfiable, and a term T
is satisfiable if there is either an
axiom Q0
or a rule Q0 :- Q1, …, Qn
in the database such that the head Q0
and T
unifies with substitution θ
and, in the case of a rule, each
θ(Q1)
, …, θ(Qn)
(i.e., each term with the most general
substitution applied) is satisfiable.
However, this is (only) a definition of satisfiability, it does not dictate a procedure for determining whether or not a term is satisfiable. Prolog commits to one (general, but powerful) procedure for determining the satisfiability of goals:
-
Prolog maintains both a substitution and a list of goals, which can be split into a prefix of goals that have been satisfied and a suffix of goals that have not yet been satisfied. Initially, the substitution is empty and all goals are unsatisfied.
-
If there are no unsatisfied goals, then the initial goals have been satisfied and (a subset of) the satisfying substitution is returned.
-
For the current (i.e., first unsatisfied) goal, Prolog iterates through the database from top to bottom. For each axiom or rule in the database, the variables are renamed and an attempt is made to unify the head with the goal.
-
Upon finding a unification with an axiom, the most general substitution is applied to the maintained substitution and list of goals, the current goal is marked as satisfied along with the location in the database where the iteration found the unifying axiom. Execution proceeds with the next unsatified goal.
-
Upon finding a unification with a rule, the most general substitution is applied and added to the maintained substitution and applied to the list of goals, the most general substitution is applied to the body of the rule, which is inserted into the list of goals immediately after the current goal, and the current goal is marked as satisfied along with the location in the database where the iteration found the unifying rule. Execution proceeds with the next unsatisfied goal (which came from the body of the rule).
-
If the iteration reaches the end of the database (without finding a head that unifies with the goal), then the current (unsatisfied) goal is discarded, the substitution of the last successful unification is undone (i.e., variables are uninstantiated), and the last satisfied goal is marked unsatisfied and resumes its database iteration. This is an instance of backtracking (which is really just depth-first search by another name).
Weather Example
Consider the following database of three axioms and one rule; the commented numbers give the position of the rule in the database.
rainy(seattle). ;; 1 rainy(rochester). ;; 2 cold(rochester). ;; 3 snowy(X) :- rainy(X), cold(X). ;; 4
See weather.P
With this database, the goal snowy©
is (only) satisfiable with the
substitution {C ↦ rochester}
:
?- snowy(C). C = rochester; no
Let’s carefully trace through the execution of snowy(C)
:
-
{}
;; ;;snowy(C)
This is the initial state of the execution: an empty subsitution, an empty list of satisfied goals, and the initial (singleton) list of unsatisfied goals.
We now try to unify
snowy(C)
with the head of each axiom and rule of the database. The first three clauses fail to unify, but the rule at position 4 will succeed. Note that before attemptying to unify, the variables of the rule are renamed, so unification is attempted with the head of the (renamed) rulesnowy(X1) := rainy(X1), cold(X1).
, yielding the most general substitution{X1 ↦ C}
. -
{X1 ↦ C}
;;snowy(C)
(at 4) ;;rainy(C)
,cold(C)
The next state of execution: the most general substitution has been incorporated into the maintained substitution and all goals, the
snowy(C)
goal is recorded as satisfied at position 4 of the database, and the body of the rule is inserted as unsatisfied goals.We now try to unify
rainy(C)
with the head of each axiom and rule of the database. The axiom at position 1 succeeds with the most general substitution{C ↦ seattle}
. -
{X1 ↦ seattle, C ↦ seattle}
;;snowy(seattle)
(at 4),rainy(seattle)
(at 1);;cold(seattle)
The next state of execution: the most general substitution has been incorporated into the maintained substitution and all goals and the
rainy(seattle)
goal is recorded as satisfied at position 1.We now try to unify
cold(seattle)
with the head of each axiom and rule of the database, but no clause succeeds. -
{X1 ↦ C}
;;snowy(C)
(at 4) ;;rainy(C)
(after 1),cold(C)
Because the sub-goal
cold(seattle)
fails, we "undo" the last successful unification. Essentially, we return to the state of execution just beforerainy(seattle)
was marked as satisfied, but with the note that the search of the database for a clause that unifies withrainy(C)
should start after position 1.Therefore, we now try to unify
rainy(C)
with the head of each axiom and rule of the database after position 1. The axiom at position 2 succeeds with the most general substitution{C ↦ rochester}
. -
{X1 ↦ seattle, C ↦ rochester}
;;snowy(rochester)
(at 4),rainy(rochester)
(at 2);;cold(rochester)
The next state of execution: the most general substitution has been incorporated into the maintained substitution and all goals and the
rainy(rochester)
goal is recorded as satisfied at position 1.We now try to unify
cold(seattle)
with the head of each axiom and rule of the database. The axiom at position 3 succeeds with the most general substitution{}
. -
{X1 ↦ seattle, C ↦ rochester}
;;snowy(rochester)
(at 4),rainy(rochester)
(at 2),cold(rochester)
(at 3) ;;The next state of execution: the most general substitution has been incorporated into the maintained substitution and all goals and the
cold(rochester)
goal is recorded as satisfied at position 3.There are now no unsatisfied goals and the initial goal has been satisfied. The interpreter emits the substitution
C = rochester
, becauseC
was the only variable mentioned in the initial goal. (In general, many variables will be instantiated and recorded in the substitution when an initial goal is satisfied, but only those variables mentioned in the initial goal will be displayed.)At this point, if the user enters
;
, then the interpreter acts as though a sub-goal failed. -
{X1 ↦ seattle, C ↦ rochester}
;;snowy(rochester)
(at 4),rainy(rochester)
(at 2);;cold(rochester)
(after 3)We "undo" the last successful unification. Essentially, we return to the state of execution just before
cold(rochester)
was marked as satisfied, but with the note that the search of the database for a clause that unifies withcold(rochester)
should start after position 3.We now try to unify
cold(rochester)
with the head of each axiom and rule of the database after position 3, but no clause succeeds. -
{X1 ↦ C}
;;snowy(C)
(at 4) ;;rainy(C)
(after 2),cold(C)
We "undo" the last successful unification. Essentially, we return to the state of execution just before
rainy(rochester)
was marked as satisfied, but with the note that the search of the database for a clause that unifies withrainy(C)
should start after position 2.We now try to unify
rainy(C)
with the head of each axiom and rule of the database after position 2, but no clause succeeds. -
{}
;; ;;snowy(C)
(after 4)We "undo" the last successful unification. Essentially, we return to the state of execution just before
snowy(C)
was marked as satisfied, but with the note that the search of the database for a clause that unifies withsnowy(C)
should start after position 4.We now try to unify
snowy(C)
with the head of each axiom and rule of the database after position 4, but no clause succeeds.At this point, there is no last successful unification to "undo", so the proof search terminates.
Logical vs Procedural Intepretation of Prolog
There are some important consequences to Prolog’s committment to one specific procedure for determining the satisfiability of goals:
-
The top-to-bottom search of the database and the left-to-right handling of sub-goals gives a deterministic and imperative semantics to searching and backtracking.
-
The ordering of clauses in the database and the ordering of terms in a body can give different results.
-
It can change the order of solutions.
-
It can lead to infinite loops.
-
It can certainly result in inefficiencies.
-
This means that Prolog deviates slightly from a pure embodiment of the what-approach to programming (describe what constitutes an answer to the problem and the execution attempts to find such an answer); sometimes, the programmer must understand Prolog’s proof search procedure and modify their program (specifically, the ordering of clauses in the database and ordering of terms in the bodies of rules) in order to achieve the correct behavior.
It is admittedly frustrating when a "logically" correct Prolog program doesn’t work as intended.
Programming in Prolog
Writing programs in Prolog requires some idioms that are not present in other languages. For many, the struggle with programming in Prolog is that Prolog seems to lack basic features that seem essential to programming.
-
No conditional expressions.
-
No functions.
-
No loops.
However, there are idiomatic ways to deal with each of these apparent lacks:
-
No conditional expressions.
-
Use a set of rules, one for each case.
-
-
No functions.
-
Use predicates with "input" and "output" arguments. (However, note that much of the elegance of Prolog comes from crafting predicates that are not biased in their interpretation of an argument as an "input" or "output".)
-
-
No loops.
-
Use recursive predicates. (The "loop" is in the proof search procedure, which always begins at the top of the database when searching for a clause that unifies with a new sub-goal.)
-
Non-recursive and Recursive Arithmetic "Functions"
Evaluation of arithmetic expressions is accomplished by the is
predicate
(which, for historical reasons, is written as infix syntax, rather than the
prefix syntax used by other alphanumeric predicates). The first (left) argument
of is
must (at the time the predicate is solved) be either an integer or a
variable and the second (right) argument must (at the time the predicate is
solved) be a ground term represetning an arithmetic expression. For example,
here is predicate cube(X,Y)
that is satisfied when Y
is equal to X3
:
cube(X,Y) :- Y is X * (X * X).
and some queries:
?- cube(3,9). no ?- cube(3,27). yes ?- cube(3,Y). Y = 27; no
Note that the requirement that the second (right) argument of is
must (at the
time the predicate is solved) be a ground term representing an arithmetic
expression means that we cannot "solve for `X`":
?- cube(X,27). Run-time error: Used uninstantiated variable _X8 in arithmetic expression
Here is a predicate pythag(A,B,C)
that is satisfied when A
, B
, and C
are
Pythagorean triple:
pythag(A,B,C) :- A > 0, B > 0, C > 0, C2 is C * C, C2 is (A * A) + (B * B).
Note that the requirement that the first (left) argument of is
must (at the
time the predicate is solved) be either an integer or a variable means that we
cannot write pythag(A,B,C)
more simply as:
pythag(A,B,C) :- A > 0, B > 0, C > 0, C * C is (A * A) + (B * B).
(In fact, the uProlog interpreter treats this as a syntax error.)
However, if we frequently needed to assert the equality of two arithmetic
expressions, then we could simply introduce a predicate aeq
("arithmetic
equal") to capture this pattern:
aeq(E1, E2) :- X is E1, X is E2. pythag(A,B,C) :- A > 0, B > 0, C > 0, aeq(C * C, (A * A) + (B * B)).
Note that this emphasizes the fact that arithmetic expressions in Prolog are
just data structures. (A * A) + (B * B)
is just a structure: the +
functor
applied to A * A
(which is the *
functor applied to A
and A
) and B * B
(which is the functor *
applied to B
and B
).
A common Prolog error is to use an arithmetic expression (a data structure) in
an argument of a predicate. For example, this does not define a predicate
sqr(X,Y)
that is satisfied when Y
is equal to X2
:
sqr(X,X * X).
which is demonstrated by some queries:
?- sqr(2, 4). no ?- sqr(2, Y). Y = (2 * 2); no
More interesting arithmetic "functions", like factorial, require multiple clauses:
fact(0, 1). fact(N, NF) :- N > 0, M is N-1, fact(M,MF), NF is N * MF.
Here, we see how Prolog deals with the lack of a conditional (multiple rules) and the lack of a loop (recursive rules).
We can also see the classic "recursive" structure:
-
base case: an axiom
-
inductive case: a (recursive) rule
There are two important components to the definition of the fact
predicate:
-
First, note that the second rule includes a
N > 0
predicate in the body. This makes the first axiom and second rule mutually exclusive. This is important because of Prolog’s proof search. If we omitted theN > 0
predicate and the goalfact(0,10)
was posed, then the first axiom would not unify, but the second rule would, and the system would proceed with the sub-goalsM1 is 0-1, fact(M1,MF1), 10 is 0 * MF1
, which would lead to afact(-1,MF1)
sub-goal, which would lead to afact(-2,MF2)
sub-goal, which would lead to afact(-3,MF3)
sub-goal, … (infinite loop). -
Second, the order of sub-goals in the second rule is not important for logical interpretation of Prolog, but is critical for the procedural interpretation. In particular, we must ensure that
MF
is a ground term before the sub-goalNF is N * MF
is solved; thus, it must occur afterfact(M,MF)
, because solving that goal will instantiateMF
.
Exercise: Write a predicate fib(N,NF)
that is satisfied when NF
is the `N`th
Fibonacci number.
Boolean "functions"
In other languages, we often need functions that return a boolean result, indicating whether or not a relationship on the arguments holds (e.g., whether the arguments are equal). In Prolog, such "functions" are simply a predicate (and there is no "boolean result"):
-
Include facts/rules for when the relationship holds (the function returns true)
-
Don’t include any facts/rules for when the relationshi does not hold (the function returns false).
For example, we can define a predicate dir_opp(D1,D2)
when two cardinal
directions are opposite:
dir_opp(north,south). dir_opp(south,north). dir_opp(east,west). dir_opp(west,east).
Of course, in addition to checking whether or not two directions are opposite:
?- dir_opp(north,south). yes ?- dir_opp(north,east). no
we can also use the predicate to determine the direction opposite another:
?- dir_opp(west,D2). D2 = east; no ?- dir_opp(D1,D2). D1 = north D2 = south; D1 = south D2 = north; D1 = east D2 = west; D1 = west D2 = east; no
This demonstrates a nice aspect of Prolog. In a language like Scheme, we would
need both a (unary) function dir_opp(d)
that returns the direction opposite
d
and a (binary) function dir_opp?(d1,d2)
that returns the boolean
indicating whether or not d1
and d2
are opposite directions. In Prolog,
both behaviors are concisely captured by a single predicate.
Acknowledgments
Portions of these notes based upon material by Hossein Hojjat.