Elements of Prolog Programs

Syntax

All Prolog statements (axioms, rules, and goals) are Horn clauses

\[\forall x_1, x_2, \ldots. (\exists y_1, y_2, \ldots. Q_1 \wedge Q_2 \wedge \cdots \wedge Q_n) \Rightarrow Q_0\]

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, …​, and Qn are provable, then Q0 is provable.

  • goal: a body (with no head)

    ?- Q1, Q2, …​, Qn.

    Meaning: Attempt to prove Q1, Q2, …​, and Qn.

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

1

1

unify with {} (an empty substitution)

1

2

do not unify

X

1

unify with {X ↦ 1}

1

X

unify with {X ↦ 1}

f(a, g(b))

f(a, g(b))

unify with {}

f(X, g(Y))

f(a, g(b))

unify with {X ↦ a, Y ↦ b}

f(X)

f(X)

unify with {}

Note that {X ↦ 1} and {X ↦ a} and {X ↦ Y} and many other substitutions can make the two terms identical, but are more restrictive than the most general substitution above.

f(X)

f(Y)

unify with {X ↦ Y} (or {Y ↦ X})

f(X, g(X))

f(a, g(b))

do not unify

f(X, g(X))

f(Y, g(Z))

unify with {Y ↦ X, Z ↦ X}

f(X, g(b))

f(a, Y)

unify with {X ↦ a, Y ↦ g(b)}

f(X, g(X))

f(a, Y)

unify with {X ↦ a, Y ↦ g(a)}

Note that {X ↦ a, Y ↦ g(X)} would not be a substitution that makes the two terms identical, because applying this substitution to the first term yields f(a, g(a)) and applying this substitution to the second term yields f(a, g(X)), which are not identical terms.

f(X, g(Y))

f(a, Y)

does not unify

Note that {X ↦ a, Y ↦ g(Y)} would not be a substitution that makes the two terms identical, because applying this substitution to the first term yields f(a, g(g(Y))) and applying this substitution to the second term yields f(a, g(Y)), which are not identical terms.

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.

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) rule snowy(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 before rainy(seattle) was marked as satisfied, but with the note that the search of the database for a clause that unifies with rainy(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, because C 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 with cold(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 with rainy(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 with snowy(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 the N > 0 predicate and the goal fact(0,10) was posed, then the first axiom would not unify, but the second rule would, and the system would proceed with the sub-goals M1 is 0-1, fact(M1,MF1), 10 is 0 * MF1, which would lead to a fact(-1,MF1) sub-goal, which would lead to a fact(-2,MF2) sub-goal, which would lead to a fact(-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-goal NF is N * MF is solved; thus, it must occur after fact(M,MF), because solving that goal will instantiate MF.

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.