Prolog: Symbolic Programming or Programation Logique
Prolog
stands for Programation Logique.
Prolog is a general-purpose logic programming language associated with artificial intelligence and computational linguistics. Prolog has its roots in first-order logic, which is a formal logic, and unlike many other programming languages, Prolog is intended as primarily a declarative programming language: the program logic is expressed in terms of relations, represented as facts and rules. A computation is initiated by running a query over these relations.
Prolog implements the predicate calculus via Horn clauses. The inference mechanism of Prolog is based upon Robinson's resolution principle (1965) together with mechanisms for extracting answers proposed by Green (1968). These ideas came together forcefully with the advent of linear resolution procedures. Explicit goal-directed linear resolution procedures, such as those of Kowalski and Kuehner (1971) and Kowalski (1974), gave impetus to the development of a general purpose logic programming system.
A logic program is a set of axioms, or rules, defining relations between objects. A computation of a logic program is a deduction of consequences of the program. A program defines a set of consequences, which is its meaning. The art of logic programming is constructing concise and elegant programs that have the desired meaning.
The meaning of a logic program P, M(P), is the set of ground goals deducible from P.
Prolog was one of the first logic programming languages, and remains the most popular among such languages today, with several free and commercial implementations available. The language has been used for theorem proving, expert systems, term rewriting, type inference, and automated planning, as well as its original intended field of use, natural language processing. Modern Prolog environments support the creation of graphical user interfaces, as well as administrative and networked applications.
Prolog is well-suited for specific tasks that benefit from rule-based logical queries such as searching databases, voice control systems, and filling templates.
Why Prolog?
Much has been written about the wonders of Prolog as a declarative programming language and its strength as a language for implementing artificial intelligence (AI) applications. A look under the hood of both AI and Prolog reveals why.
Prolog features a collection of search and pattern- matching algorithms. This, it turns out, is the essence of much AI programming. A chess program searches for patterns in the game, a natural language program searches for patterns in lists of words, and a diagnostic program searches for rules that match symptoms. Prolog is very good at pattern matching and search.
Two features in a programming language that make pattern-matching easier are 1) support for symbols as a primitive data type that can be manipulated without the need to call special functions, and 2) dynamic memory management so the developer can simply use the symbols without worrying about memory allocation issues. Languages that have these features, such as Prolog and LISP, are called symbolic languages.
Consider, for example, a simple control loop that reads a command from a user and then does something. In C that code might look like
void main()
{
char buf[20];
do {
gets(buf);
if (0 == strcmp(buf, "open")) ...
else if (0 == strcmp(buf, "add")) ...
else if (0 == strcmp(buf, "delete")) ...
} while (strcmp(buf, "quit"));
printf("done");
}
In Prolog, dynamically allocated symbols are used instead of character strings, so the equivalent code looks like
main :-
repeat,
read(X),
do(X),
X == quit,
write(done).
do(open) :- ...
do(add) :- ...
do(delete) :- ...
Notice the lack of data definition statements or string compares. The difference is not significant in a simple example such as this, but in applications where the bulk of the work is comparing symbols, it becomes quite significant.
In addition to dynamically allocated symbols, Prolog has, as an integral part of the language, a sophisticated pattern matching algorithm, called unification, and a search mechanism, called backtracking.
These can both be seen in the code fragment above. The pattern do(X) is unified against the first of the three do rules, defined by the :- symbol meaning if
. If the user entered open then the first clause, as it's called, would match and the code on the right of the :- would be executed. If the user entered something other than open, Prolog would backtrack and continue to look for a do rule that does match the user's input.
Similarly, the use of the repeat and X == quit in the main rule causes the sample section of code to loop until the user types quit. The Prolog programmer doesn't write if-then-elses
, calls
, whiles
, or other flow-of-control constructs, however, between unification and backtracking the programmer can induce any flow-of-control behavior in a Prolog program that can be achieved in any other language.
Symbols, unification, backtracking, and dynamic memory management, all tend to eliminate procedural code a programmer normally needs to write. It is no surprise that what is left looks much more declarative than conventional code and is often a fraction of the size. For example, Prolog can be used to analyze simple English sentences. In fact Prolog was originally designed for working with language. As such it is well suited for not just natural language work, but for implementing and/or experimenting with formal languages as well.
One common use in this line is to build shells for implementing expert systems. The shells use their own language to represent the knowledge for a particular type of problem. Diagnostic systems, for example, work differently from configuration systems.
A tax application in Prolog is simply a collection of rules that specify how to fill out each line of the form. Prolog's search and unification do the work of linking related lines of the form together so the program code winds up looking very much like the tax form itself. Business applications such as customer order entry are expressed as relationships describing the various transactions and constraints. Pricing and configuration, two difficult components of such a system, are relatively straight-forward when coded in Prolog.
While C can be used to write anything written in Prolog, Prolog code, for the applications it does best, is much less complex. For this reason, the Prolog developer can manage and maintain a greater degree of complexity in an application, thus providing a greater degree of sophistication to the user of an application.
For commercial applications, this leads to a competive edge, but the real benefit applies to any application-- Prolog is simply fun.
How Prolog is Used
The examples above provide insight into how a Prolog program is used. The grammar rules, for example, are a Prolog program. They are queried with lists of words. All Prolog programs are similar collections of rules, and are similarly activated by being queried, much the same way a database is queried.
This is true even of stand-alone compiled Prolog programs. In that case there is usually a single query, main, used to start the program. main is the name of a rule which then queries other rules which query other rules and so on. For example, the following Prolog program could be compiled and executed, with a classic result.
main :- write('Hello World').
It could also be loaded and run from an interpreter.
?- main. Hello World
Starting Prolog
Consider the following problem description:
Three friends come first, second, and third in a tournament. Each of them has a different first name, likes a different sport and has a different nationality.
Michael likes basketball and did better than the American. Simon, the Israeli, did better than the tennis player. The cricket player came first.
A prolog shell may be started for the user-programmer to work interactively. Alternatively, you may cause a shell to read a database file, for instance by issueing consult(my_database). Alternatively, type [my_database].
Warning: The database name must be stripped of its extension
The Building Blocks of a Prolog Program
Facts
We first discuss facts about objects. Suppose we want to tell Prolog the fact that John likes Mary
. This fact consists of two objects, called Mary
and John
, and a relationship or predicate, called likes
. In Prolog, we need to write facts in a standard form, like this:
likes(john, mary).
The following things are important:
- The names of all relationships and objects must begin with a lower-case letter. For example, likes, john, mary.
- The relationship is written first, and the objects are written separated by commas and enclosed in round brackets.
- The dot character
.
must come at the end of a fact. The dot is what some people also call aperiod
or afull stop
.
Questions
Once we have some facts, we can ask some questions about them. In Prolog, a question looks just like a fact, except that we put a special symbol before it. The special symbol is written as a question mark followed by a hyphen. Consider the question:
?- owns(mary, book).
If we interpret mary to be a person called Mary, and book to be some particular book, this question is asking Does Mary own the book?
, or Is it a fact that Mary owns the book?
We are not asking whether she owns all books, or books in general, because the first letter of book is lower case.
When a question is asked of a Prolog system, it will search through the database. It looks for facts that unify the fact in the question. Two facts unify if their predicates are the same (spelled the same way), and if their corresponding arguments each are the same. If Prolog finds a fact that unifies with the question, Prolog will respond yes. If no such fact exists in the database, Prolog will respond no.
Rules
In Prolog, rules are used when you want to say that a fact depends on a group of other facts. Rules are also used to express definitions. A rule is a general statement about objects and their relationships. For example, we can say that Fred is a bird
if Fred is an animal and Fred has feathers, and we can also say that Bertram is a bird if Bertram is an animal and Bertram has feathers. So, we can allow a variable to stand for a different object in each different use of the rule.
In Prolog, a rule consists of a head and a body. The head and body are connected by the symbol :-
, which is made up of a colon and a hyphen. The :-
is pronounced if
.
That John likes whoever likes wine is written in Prolog as:
likes(john, X) :- likes(X, wine).
Notice that rules also end with a dot (actually a period
or full stop
character). The head of this rule is likes(john, X). The head of the rule describes what fact the rule is intended to define. The body, in this case likes(X, wine), describes the conjunction of goals that must be satisfied, one after the other, for the head to be true. For example, we can make John more choosy about whom he likes, simply by adding more goals onto the body, separated by commas:
likes(john, X) :- likes(X, wine), likes(X, food).
or, in words, John likes anyone who likes wine and food. Or, suppose John likes any female who likes wine:
likes(john, X) :- female(X), likes(X, wine).
And so on.
Input, Output, and Input-Output Arguments to Predicates in Prolog
An argument may be either of them three and change its role according to the situation, for there is nothing in the syntax of prolog to indicate which.
Functors or Structures
A structure is written in Prolog by specifying its functor and its components. The functor names the general kind of structure, and corresponds to a datatype in an ordinary programming language. The components are enclosed in round brackets and separated by commas. The functor is written just before the opening round bracket. Consider the following fact, that John owns the book called Wuthering Heights, by Emily Bronte:
owns(john, book(wuthering_heights, bronte)).
Inside the owns fact we have a structure by the name of book, which has two components, a title and an author. Since the book structure appears inside the fact as one of the fact's arguments, it is acting as an object, taking part in a relationship. If we like, we can also have another structure for the author's name, because there were three Bronte writers we wish to distinguish:
owns(john, book(wuthering_heights, author(emily, bronte))).
Structures may participate in the process of question-answering using variables. For example, we may ask if John owns any book by any of the Bronte sisters:
?- owns(john, book(X, author(Y, bronte))).
And so on.
If you have guessed that the syntax for structures is the same as for Prolog facts, you are correct. A predicate (used in facts and rules) is actually the functor of a structure. The arguments of a fact or rule are actually the components of a structure. There are many advantages to representing Prolog programs themselves as structures. It is not important to know why just now, but do keep in mind that all parts of Prolog, even Prolog programs themselves, are made up of constants, variables, and structures.
Horn Clauses
In mathematical logic and logic programming, a Horn clause is a logical formula of a particular rule-like form which gives it useful properties for use in logic programming, formal specification, and model theory. Horn clauses are named for the logician Alfred Horn, who first pointed out their significance in 1951.
The Wikipedia
In logic programming, a Horn clause is an implication from a conjunction of none, one or several goals.
Jess will come if it is sunny AND her car starts, AND her mother is not ill.
This might be coded in prolog like this:
come(jess) :- sunny, start(car(jess)), not ill(mother(jess)).
A Simple Database of Prolog Facts and Rules
This is a simple database to establish if two people are friends:
friends(X,Y) :- likes(X,Z), likes(Y,Z). likes(john,gin). likes(peter,gin). likes(andy,beer).
Symmetric Relationships in Prolog
The following database and program lead off to infinite loops when consulted (has_met(X,a).):
% database: has_met(a,b). has_met(a,c). % rules: has_met(X,Y) :- has_met(Y,X).
The following one, though, behaves well:
% forward 'has_met' data base: has_met_fwd(a,b). has_met_fwd(a,c). has_met(X,Y) :- has_met_fwd(X,Y) ; has_met_fwd(Y,X).
Alternatively:
% forward 'has_met' data base: has_met_fwd(a,b). has_met_fwd(a,c). % rules: has_met(X,Y) :- has_met_fwd(X,Y). has_met(X,Y) :- has_met_fwd(Y,X).
Which obligingly yields:
| ?- has_met(X,a). X = b ? ; X = c yes
Transitivity
Here is a slightly more complicated example which codes some transitivity.
% database: tr(a,b). tr(b,c). tr(c,d). tr(bb,c). tr(d,e). tr(e,f). % rules: transitive(X,Y) :- tr(X,Y). transitive(X,Z) :- tr(X,Y), transitive(Y,Z).
And this is the result of a difficult query:
| ?- transitive(X,f). X = e ? ; X = a ? ; X = b ? ; X = c ? ; X = bb ? ; X = d ? ; no
Equivalence
Next, we would like to code an equivalence relationship.
...
...
...
Lists in Prolog
Writing a list to standard output is a nice exercise for understanding both lists and recursive control:
writelist([]). writelist([H|T]) :- write(H),nl,writelist(T).
Membership in Prolog
Membership is established through recursion. I don't like it, though.
member(X,[X|_]). member(X,[_|T] :- member(X,T).
Appending
The arguments to non-built-in append/3 predicate are the initial sublist and the remaining sublist of a list, which is the third argument. It may be defined as:
append([], X, X). append([A|B], C, [A|D]) :- append(B, C, D).
Here is an example of execution of a query (find the lists X and Y such that the concatenation of X and Y is [a,b]):
| ?- append(X,Y,[a,b,c]). X = [] Y = [a,b,c] ? ; X = [a] Y = [b,c] ? a % here the user presses a to compute all remaining solutions X = [a,b] Y = [c] X = [a,b,c] Y = [] no
Backtracking
Briefly, the backtracking algorithm examines each predicate specification in the order that it was placed in the Prolog knowledge base. If the variable bindings of the specification satisfy the query it accepts them. If they don't, the interpreter goes on to the next specification. If the interpreter runs into a dead end, i.e., no variable substitution satisfies it, then it backs up looking for other variable bindings for the predicates it has already satisfied.
Backtracking: Cuts and Repeats
We can imagine a conjunction of goals to be arranged from left to right, separated by commas. When handling such a conjunction of goals, Prolog attempts to satisfy each goal in turn, working from left to right. If a goal becomes satisfied, Prolog leaves a place-marker in the database that is associated with the goal. Think of this as drawing an arrow from the goal to the place in the database where the solution is. Furthermore, any variables previously uninstantiated might now be instantiated. If a variable becomes instantiated, all its occurrences in the question become instantiated, too. Prolog then attempts to satisfy the goal's right-hand neighbour, starting from the top of the database.
As each goal in turn becomes satisfied, it leaves behind a place-marker in the database (draws another arrow from the goal to the unifying fact), in case the goal needs to be re-satisfied at a later time. Any time a goal fails (cannot find a unifying fact), Prolog goes back and attempts to satisfy its left-hand neighbour, starting from its place-marker. Furthermore, Prolog must uninstantiate
any variables that became instantiated at this goal. In other words, Prolog must undo
all the variables when it re-satisfies a goal. If each goal, upon being entered from its right, cannot be re-satisfied, then the failures will cause Prolog to gradually creep to the left as each goal fails. If the first goal (the left-most goal) fails, then, since it does not have a left-hand neighbour it can attempt to re-satisfy, the entire conjunction fails. This behaviour, where Prolog repeatedly attempts to satisfy and re-satisfy goals in a conjunction, is called backtracking.
The Use of Cut to Prevent Backtracking in Prolog
Sometimes it is desirable to selectively turn off backtracking. Prolog provides a predicate that performs this function. It is called the cut, represented by an exclamation mark (!).
The cut effectively tells Prolog to freeze all the decisions after it occurs in a predicate. That is, if required to backtrack, Prolog will automatically fail without trying other alternatives.
The following database and query seems to fail:
related(X,Y) :- related(X,Z), related(Z,Y). related(phil, roxanne). related(roxanne, carol). related(carol, alex).
Followed by the query:
related(phil,carol).
Performance is the main reason to use the cut. This separates the logical purists from the pragmatists. Various arguments can also be made as to its effect on code readability and maintainability. It is often called the goto
of logic programming.
You will most often use the cut when you know that at a certain point in a given predicate, Prolog has either found the only answer, or if it hasn't, then there is no answer. In this case you insert a cut in the predicate at that point.
Similarly, you will use it when you want to force a predicate to fail in a certain situation, and you don't want it to look any further.
Some Examples of the Use of Cuts
The following program defines the minimum predicate with cuts:
minimum(X, Y, X) :- X =< Y, !. minimum(X,Y,Y) :- X > Y, !.
...
repeat
The built-in predicate repeat is provided as an extra way to generate multiple solutions through backtracking. Although it is built-in, it can be thought of as behaving as though defined as follows:
repeat. repeat :- repeat.
What is the effect of this if we put repeat as a goal in one of our rules? First of all, the goal will succeed, because of the fact which is the first clause of repeat. Secondly, if backtracking reaches this point again, Prolog will be able to try an alternative: the rule that is provided as the second clause of repeat. When it uses this rule, another goal repeat is generated. Since this matches the first fact, we have succeeded again. If backtracking reaches here again, Prolog will again use the rule where it used the fact before. To satisfy the extra goal generated, it will again pick the fact as the first option. And so on. In fact, the goal repeat will be able to succeed infinitely many times on backtracking. Note the importance of the order of the clauses here. (What would happen if the fact appeared after the rule?).
Why is it useful to generate goals that will always succeed again on backtracking? The reason is that they allow one to build — from rules that have no choices in them — rules that do have choices. And we can make them generate different values each time.
Loops with repeat/0
Furthermore, a clause body with a repeat/O followed by fail/O will go back and forth forever. This is one way to write an endless loop in Prolog.
A repeat/O followed by some intermediate goals followed by a test condition will loop until the test condition is satisfied. It is equivalent to a do until
in other languages.
command_loop :-
repeat,
write('Enter command (end to exit): '),
read(X),
write(X), nl,
X = end.
Unification, the Engine for Predicate Matching and Evaluation
When queried, a prolog program performs a series of resolutions on its database entries, rather than sequentally evaluating statements and expressions like a traditional language. This has an important ramification: variables are bound (assigned values, instantiated,...) by unification and not by evaluation unless an evaluation is explicitly requested.
One consequence of this is the relaxation of the requirement to specify variables (function parameters) as input or output.
Unification is a powerful technique for rule-based and frame-based expert systems, where it matches case-specific data to the rules that best meet these specifications. All production systems require a form of this matching, and it is often necessary to write a unification algorithm in languages that don't provide one.
Forcing Evaluation with Operator is
An important difference between unification-based computing and the use of more traditional languages is that unification performs syntactic matches (with appropriate parameter substitution) on structures. It does not evaluate expressions.
Prolog provides an operator, is, that performs arithmetic evaluation. It evaluates the expression on its right-hand side and attempts to unify the result with the object on its left. Thus,
X is Y + Z
unifies X with the value of Y added to Z. Because it performs arythmetic evaluation:
- If Y and Z do not have values (are not bound at execution time) evaluation of the
iscauses a run-time error. Therefore - X is Y + Z cannot (as one might think with a declarative programming language) assign a value to Y when X and Z are bound.
- Programs must use
isto evaluate expressions containing arithmetic operators, +, -, *, /, andmod.
Finally, as in the predicate calculus, variables in prolog may have one and only one binding. Once given a value, through local assignment or unification, they can never take on a new value, except through a backtrack in the and/or search space of the current interpretation. Thus, is does not function like a traditional assignment operator; an expression such as X is X + 1 will always fail.
Conjunction, disjunction, if-then
These are realized through operators ,, ;, and ->.
The Gory Details
Goal1 , Goal2 executes Goal1 and, in case of success, executes Goal2.
Goal1 ; Goal2 first creates a choice-point and executes Goal1. On backtracking Goal2 is executed.
Goal1 -> Goal2 first executes Goal1 and, in case of success, removes all choice-points created by Goal1 and executes Goal2. This control construct acts like an if-then (Goal1 is the test part and Goal2 the then part). Note that if Goal1 fails then ->/2 fails also. ->/2 is often combined with ;/2 to define an if-then-else as follows: Goal1 -> Goal2 ; Goal3. Note that Goal1 -> Goal2 is the first argument of the (;)/2 and Goal3 (the else part) is the second argument. Such an if-then-else control construct first creates a choice-point for the else-part (intuitively associated with ;/2) and then executes Goal1. In case of success, all choice-points created by Goal1 together with the choice-point for the else-part are removed and Goal2 is executed. If Goal1 fails then Goal3 is executed.
Negation by Failure in Prolog
A goal G fails (or not g succeeds), if G cannot be derived from the consulted database.
Here is how we might define operator not:
not X :- X, !, fail. not X.
...
Prolog Programming Styles
There are two basic styles of developing logic programs: defining a logical database, and manipulating data structures.
A logic database comprises a set of facts and rules. A set of facts can define relations, as in relational databases. And rules can define complex relational queries, as in relational algebra.
Data structures in Prolog are based on lists.
Prolog Operators
Sometimes it is convenient to write some functors as operators. This is a form of syntax that makes some structures easier to read. For example, arithmetic operations are commonly written as operators. When we write the arithmetic expression plus
sign and the multiply
sign operators. If we had to write the arithmetic expression
(The same would be expressed in functional notation as add(x,multiply(y,z)) given a previous definition of add and multiply.)
The operators are sometimes easier to use, however, because we have grown accustomed to using them in arithmetic expressions ever since our schooldays. Also, the structure form requires that round brackets be placed around the functor's components, which may be awkward at times.
It is important to note that the operators do not cause any arithmetic to be carried out. So in Prolog, 3+4 does not mean the same thing as 7. The term 3+4 is another way to write the term +(3,4), which is a data structure.
First we need to know how to read arithmetic expressions that have operators in them. To do this, we need to know three things about each operator: its position, its precedence, and its associativity. In this section we will describe how to use Prolog operators with these three things in mind, but we will not go into very much detail. Although many different kinds of operators can be made up, we shall deal only with the familiar atoms +, -, *, and /.
The precedence of an operator is used to indicate which operation is carried out first. Each operator that is used in Prolog has a precedence class associated with it. The precedence class is an integer that is associated with an operator. The exact value of the integer depends on the particular version of Prolog you are using. However, it is always true that an operator with a higher precedence has a precedence class that is closer to 1. If precedence classes range from 1 to 255, then an operator in the first precedence class is carried out first, before operators belonging to the 129th (say) precedence class. In Prolog the multiplication and division operators are in a higher precedence class than addition and subtraction, so the term a-b/c is the same as the term -(a,/(b,c)). The exact association of operators to precedence classes is not important at the moment, but it is worth remembering the relative order in which operations are carried out.
Finally, consider how different operators associate. How they associate comes to our attention when we have several operators of the same precedence. When we see the expression
Equality and Unification
There really is no equivalent of the assignment in prolog, and you should not use the dynamic database for this purpose.
One noteworthy built-in predicate is equality, which is an infix operator written as =. When an attempt is made to satisfy the goal
?- X = Y.
(pronounced X equals Y
), Prolog attempts to unify X and Y, and the goal succeeds if they unify. We can think of this act as trying to make X and Y equal. The equality predicate works as though it were defined by the following fact:
X = X.
Strict Equality (X == Y)
The predicate == represents a much stricter equality test than =. That is, if X == Y ever succeeds then X = Y does as well. On the other hand, this is not so the other way round. The way that == is more strict is by the way it considers variables. The = predicate will consider an uninstantiated variable to be equal to anything, because it will match anything. On the other hand, == will only consider an uninstantiated variable to be equal to another uninstantiated variable that is already sharing with it. Otherwise the test will fail. So we get the following behaviour:
?- X == Y. no ?- X == X. X = _23 ?- X=Y, X==Y. X = _23, Y = _23 ?- append([A|B], C) == append(X, Y). no ?- append([A|B], C) == append([A|B], C). A = _23, B = _24, C = _25
Arithmetic in Prolog
Arithmetic can be used for calculating. For example, if we know the population and ground area of a country, we can calculate the population density of the country. The population density tells us how crowded the country would be if all the people were evenly spread throughout the country. Consider the following database about the population and area of various countries in 1976. We will use the predicate population to represent the relationship between a country and its population. Nowadays, the populations of countries are generally quite large numbers. So, we will represent population figures in millions: population(X, Y) means the population of country X is about Y million people
. The predicate area will denote the relationship between a country and its area (in millions of square miles). The numbers given here are not exact, but they will do for the purpose of demonstrating arithmetic:
population(usa, 203). population(india, 548). population(china, 800). population(brazil, 108). area(usa, 3). area(india, 1). area(china, 4). area(brazil, 3).
Now to find the population density of a country, we must use the rule that the density is the population divided by the area. This can be represented as the predicate density, where the goal density(X,Y) succeeds for country X having Y as the population density of that country. A Prolog rule for this is:
density(C, Y) :- population(C, P), area(C, A), Y is P / A.
We need to use built-in infix predicator is to cause evaluation and then unification.
Standard Arithmetic Operators in Prolog
Depending on what computer you use, various arithmetic operators can be used
on the right-hand side of the is operator. All Standard Prolog systems, however, will have the following, as well as many more:- X + Y: the sum of X and Y
- X - Y: the difference of X and Y
- X * Y: the product of X and Y
- X / Y: the quotient of X divided by Y
- X // Y: the integer quotient of X divided by Y
- X mod Y: the remainder of X divided by Y
Numerical Comparison Operators in Prolog
Given two numbers, we can tell whether one number is equal to the other, or less than the other, or greater than the other. Prolog provides certain built-in predicates for comparing numbers. Actually these predicates evaluate terms that are treated as arithmetic expressions. The arguments could be variables instantiated to integers, or they could be integers written as constants, or they could be more general expressions. Here we will use these predicates just for comparing numbers, but later we'll use them in general arithmetic expressions. Note that we are allowed to write them as infix operators:
- X =:= Y: X and Y stand for the same number
- X =\= Y: X and Y stand for different numbers
- X < Y: X is less than Y
- X > Y: X is greater than Y
- X =< Y: X is less than or equal to Y
- X >= Y: X is greater than or equal to Y
Note that the less than or equal to
symbol is not written as <=
as in many programming languages. This is done so that the Prolog programmer is free to use the <=
atom, which looks like an arrow, for other purposes.
The equality predicate, when applied on numbers, succeeds when two number arguments are the same. However, if one of the arguments is a variable, the equality predicate will cause the variable to be instantiated because the equality predicate performs a unification of its two arguments. In many numeric calculations, this is not desirable. Instead, Prolog makes available predicates specifically for comparing equality and inequality of numbers. In all of the following predicates, both arguments must be instantiated, or an error occurs. Using these predicates for numeric calculation can also cause the program to be executed more efficiently.
This numeric equality predicate succeeds when the left-hand number argument is equal to the right-hand number argument.
This numeric equality predicate succeeds when the left-hand number argument is not equal to the right-hand number argument.
Comparing Arbitrary Terms
They start with character @: @<, @>, @>=, and @=<.
Here are the principles that determine whether one term is considered less than another:
- All uninstantiated variables are less than all floating-point numbers, which are less than all integers, which are less than all atoms, which are less than all structures.
- For two non-sharing uninstantiated variables, one will be less than the other (which one is less may be different in different Prolog implementations). A floating point number is less than another floating point number or an integer is less than anothe integer in the expected way.
- One atom is less than another if it would come earlier than it in the normal dictionary ordering. To be precise, the ordering depends on the character codes, but these are usually ordered as one would expect, at least for alphabetic characters.
- One structure is less than another if its functor has a lower arity. If two structures have the same arity, one is less than the other if its functor is less than the other (using the ordering for atoms). If two structures have the same arity and functor, they are ordered by considering the arguments in turn – for the first corresponding arguments that differ, the order of the structures is the order of the relevant arguments.
Declaring Operators with Predicate op/3
In Prolog, if we wish to declare that an operator with a given position, precedence class, and associativity is to be recognised when terms are read and written, we use the built-in predicate op. If Name is the desired operator (the atom that we want to be an operator), Prec the precedence class (an integer within the appropriate range), and Spec the position/associativity specifier (one of the above atoms), then the operator is declared by providing the following goal:
?- op(Prec, Spec, Name).
As an example of declaring operators, the following is a list of the most important operators that are already defined in Standard Prolog:
?- op( 1200, xfx, ':-' ). ?- op( 1200, fx, '?-' ). ?- op( 1200, fx, ':-' ). ?- op( 1100, xfy, ';' ). ?- op( 1000, xfy, ',' ). ?- op( 900, fy, '\+' ). ?- op( 700, xfx, '='). ?- op( 700, xfx,\=). ?- op( 700, xfx, '==' ). ?- op( 700, xfx, \== ). ?- op( 700, xfx, '=..' ). ?- op( 700, xfx, '<' ). ?- op( 700, xfx, '>' ). ?- op( 700, xfx, '=<' ). ?- op( 700, xfx, '>=' ). ?- op( 700, xfx, '@<' ). ?- op( 700, xfx, '@=<' ). ?- op( 700, xfx, '@>' ). ?- op( 700, xfx, '@>=' ). ?- op( 700, xfx, 'is' ). ?- op( 500, yfx, '+' ). ?- op( 500, yfx, '-' ). ?- op( 400, yfx, '*' ). ?- op( 400, yfx, '//' ). ?- op( 400, yfx, '/' ). ?- op( 400, yfx, 'mod' ). ?- op( 200, fy, '-').
...
An operator declared yfx is left associative. Similarly, an operator declared xfy is right associative.
Note that the meanings of x and y (in terms of what other operators can appear unbracketed in the relevant position) are the same in all the other cases as well. This means that, for instance, the sequence
not not a
is legal syntactically if not is declared as fy, but is illegal if it is declared fx.
...
File I/O in Prolog
Prolog Input and Output
Here are some predicates for reading from the input stream and writing to the output stream:
Reading and writing one character
The goal get_char(X) succeeds if X can be matched with the next character encountered on the current input stream. get_char succeeds only once (it cannot be re-satisfied). The operation of moving to the next character is not undone on backtracking, because there is no way to put a character back onto the current input stream.
The goal put_char(X) writes the character X on the current output stream. put_char succeeds only once. An error occurs if X is not instantiated.
Reading and Writing a Term
The goal read(X) reads the next term from the current input stream and matches it with X. A read succeeds only once. The term must be followed by a dot .
, which does not become a part of the term, and at least one non-printing character. The dot is removed from the current input stream
The goal write(X) writes the term X to the current output stream. write succeeds only once. Any uninstantiated variables in X are written as uniquely numbered variables beginning with an underscore, such as _239
. Co-referring variables within the same argument to write have the same number when they are printed out. The predicate write takes account of current operator declarations when it prints a term. Thus an infix operator will be printed out between its arguments, for instance.
The predicate write_canonical works in exactly the same way as write, except that it ignores any operator declarations. When write_canonical is used, any structure is printed out in prefixed notation with the functor first and the arguments in brackets afterwards.
Effecting a New Line
The goal nl writes a control sequence to the current output stream that causes a new line
. On a computer display, all characters after the use of nl appear on the next line of the page. nl succeeds only once.
Handling Files
The following predicates open and close files, get file handles, and set current input and output streams.
This goal opens a file whose name is X (an atom). If Y is read then the file is opened for reading; otherwise if Y is write then the file is opened for writing. Z is instantiated to a special term naming the stream that must be referred to when the file is accessed later. An error occurs if X is not instantiated, or if X names a file that does not exist.
This is used when X is a term naming a stream. The stream is closed and can no longer be used.
Sets the current input to the stream whose name is provided by X. X will be a term returned in the third argument of open, or the atom user_input, which specifies that input is to come from the keyboard.
Sets the current output to the stream whose name is provided by X. X will be a term returned in the third argument of open, or the atom user_output, which specifies that output is to go to the computer display.
This goal succeeds if the name of the current input stream matches with X, and fails otherwise.
This goal succeeds if X matches with the name of the current output stream, and fails otherwise.
Prolog Meta-Predicates
These predicates query and manipulate other predicates rather than the terms or objects that the other, non-meta predicates denote. Some of them are:
var(X), which succedes only when X is an unbound variable;nonvar(X), which succedes only when X is bound to a nonvariable term;=.., which creates a list from a predicate term; for instance, jump(tick,3)=..X unifies X with [jump,tick,3];functor(A,B,C)succeeds with A a term whose principal functor has name B and arity C; this predicate can be used to produce a predicate or to to produce all the terms with a given name and/or arity.clause(A,B)unifies B with the body of a clause whose head unifies with A;any_predicate(...,P,...) :- Pexecutes a predicate P, the argument of an arbitrary predicate: a predicate, here P, may be passed as a parameter and executed at any desired time in the computation;call(C), where C is a clause, also succeeds with the execution of predicate C;
Next some of these predicates will be explained further:
Satisfying a goal of the form clause(X, Y) causes X and Y to be matched with the head and body of an existing clause (for a public
predicate) in the database. When an attempt is made to satisfy the goal, X must be instantiated enough so that the main predicate of the clause is known. If there are no clauses for the predicate, the goal just fails. If there is more than one clause that matches, Prolog will choose the first one. In this case, if an attempt is made to re-satisfy the goal, the other matching clauses will be chosen, one at a time.
Notice that, although clause always has an argument for the body of a clause, not every clause actually has a body. If a clause does not have a body, it is considered to have the dummy body true. We have been calling such clauses facts
. By providing X's and Y's that are more or less instantiated, you can look for either all the clauses for a given predicate and number of arguments, or all the ones that match some pattern.
The predicate functor is defined in such a way that functor(T,F,N) means, T is a structure with functor [name] F and arity (number of arguments) N
. It can be used in basically two ways. In the first way, T is already instantiated. The goal fails if T is not an atom or a structure. If T is an atom or structure, F is matched with the functor and N is matched with the integer giving the arity (number of arguments) of the functor. Note that in this context, an atom is considered to be like a structure with arity 0.
Here are some examples of goals involving functor:
?- functor(f(a, b, g(Z)), F, N). Z = _23, F = f, N = 3 ?- functor(a + b, F, N). F = +, N = 2 ?- functor([a, b, c], F, N). F = ., N = 2 ?- functor(apple, F, N). F = apple, N = 0 ?- functor([a, b, c], '.', 3). no ?- functor([a, b, c], a, Z). no
There is a second use to functor. This occurs when the first argument of the goal (T) is uninstantiated. In this case, both of the others must be instantiated: specifying a functor and a number of arguments respectively. A goal of this form will always succeed, and as a result T will become instantiated to a structure with the functor and number of arguments provided. So this is a way of constructing arbitrary structures, given a specification in terms of a functor [name] and its number of arguments. The arguments of such a structure constructed by functor are uninstantiated variables. Hence the structure will match any other structure with the same functor and number of arguments.
Copying an Existing Structure with function/3
A common use of functor to create a structure is when we wish to make a copy
of an existing structure with new variables as the arguments of the principal functor. We can encapsulate this use in the definition of a predicate copy, as follows:
copy(Old, New) :- functor(Old, F, N), functor(New, F, N).
Here, two functor goals occur adjacently. If the copy goal has the first argument instantiated and the second uninstantiated, then the following will happen. The first functor goal will involve the first possible use of the predicate (because the first argument will be instantiated). Hence F and N will become instantiated to the functor and number of arguments of this existing structure. The second functor goal uses the predicate in the second way. This time the first argument is uninstantiated, and the information in F and N is used to construct the structure New. This is a structure involving the same functor and number of arguments as Old, but with variables as its components. Thus we would get interactions like:
?- copy(sentence(np(n(john)), v(eats)), X). X = sentence(_23, _24)
Accessing Arguments in a Structure Through arg(N,T,A)
The predicate arg must always be used with its first two arguments instantiated. It is used to access a particular argument of a structure. The first argument of arg specifies which argument is required. The second specifies the structure that the argument is to be found inside. Prolog finds the appropriate argument and then tries to match it with the third argument. Thus arg(N, T, A) succeeds if the Nth argument of T is A. Let us look at some goals involving arg.
?- arg(2, related(john, mother(jane)), X). X = mother(jane) ?- arg(1, a+(b+c), X). X=a ?- arg(2, [a,b,c], X). X = [b,c] ?- arg(1, a+(b+c), b). no
Sometimes we will want to use functor and arg when the possible structures are known. This is because there may be so many arguments that it is inconvenient to specify them every time. Consider an example where we use structures to represent books. We might have a component for the title, the author, the publisher, the date of publication, and so on. Let us say that the resulting structures have fourteen components. We might write the following useful definitions:
is_a_book(book(_,_,_,_,_,_,_,_,_,_,_,_,_,_)). title(book(T,_,_,_,_,_,_,_,_,_,_,_,_,_), T). author(book(_,A,_,_,_,_,_,_,_,_,_,_,_,_), A).
In fact, we can write these much more compactly as:
is_a_book(X) :- functor(X, book, 14). title(X, T) :- is_a_book(X), arg(1, X, T). author(X, A) :- is_a_book(X), arg(2, X, T).
The predicates functor and arg provide one way of creating and accessing arguments of arbitrary structures. The predicate =.. (pronounced univ
for historical reasons) provides an alternative way, which is useful if you want to obtain the arguments of a structure all together, or if you want to construct a structure, given a list of arguments.
The goal X =.. L means, L is the list consisting of the functor of X followed by the arguments of X.
Such a goal can be used in two ways, in the same way that a functor goal can. If X is instantiated, Prolog constructs the appropriate list and tries to match it with L. Alternatively, if X is uninstantiated, the list will be used to construct an appropriate structure for X to stand for. In this case, the head of L must be an atom (it will become the functor of X). Here are some examples of =.. goals:
?- foo(a,b,c) =.. X. X = [foo,a,b,c] ?- [a,b,c,d] =.. L. L = ['.',a,[b,c,d]]. ?- (a+b) =.. L. L = [+,a,b]. ?- (a+b) =.. [+,X,Y]. X = a, Y = b. ?- [a,b,c,d] =.. [X|Y]. X = '.', Y = [a,[b,c,d]] ?- X =.. [a,b,c,d]. X = a(b,c,d)
It is assumed that X is instantiated to a term that can be interpreted as a goal. The call(X) goal succeeds if an attempt to satisfy X succeeds. The call(X) goal fails if an attempt to satisfy X fails. At first sight, this predicate may seem redundant, because one might ask why the argument of call shouldn't simply appear by itself as a goal?
For instance, the goal
..., call(member(a, X)),...
can always be replaced by
..., member(a, X),...
However, if we are constructing goals by using the =.. predicate or functor and arg, then it is possible to call goals that have a functor that is unknown at the time you type in your program.
Assuming that P, X, and Y are instantiated to a functor and arguments appropriately, call can be used as follows:
..., Z =.. [P,X,Y], call(Z),...
Negating with \+ X
The \+ predicate (pronounced not
) is declared as a prefix operator. It is assumed that X is instantiated to a term that can be interpreted as a goal. The \+ X goal succeeds if an attempt to satisfy X fails. The \+ X goal fails if an attempt to satisfy X succeeds. In this way, \+ is rather like call, except that the success or failure of the argument, interpreted as a goal, is reversed.
Adding and Removing Rules
You use assert, asserta, assertz and retract to add and remove rules. They will only work on the definitions of predicates which have been declared dynamic
. The intention again is to prevent accidental unplanned changing of definitions. A predicate foo/4 can be declared as dynamic
by including the following in the relevant program file (before the definition of foo/4 or, if there is no definition, before the code that uses asserta etc.):
:- dynamic foo/4.
Several predicates can be declared as dynamic on one line by separating them with commas, e.g:
:- dynamic foo/4, baz/3.
Warning: These commands are deprecated because they create and remove global structures, introduce side effects and may cause other problems associated with poorly structured programs.
Working with Characters
The following two predicates compose and decompose an atom into its characters.
atom_chars(A,L)
Predicate atom_chars relates an atom to the list of characters (atoms with one element) that make it up. This can be used either to find the characters for a given atom, or to find the atom that has some given characters. The goal atom_chars(A,L) means that the characters for the atom A are the members of the list L
. If the argument A is instantiated, Prolog creates the list of characters and tries to match them with L. Otherwise Prolog uses the list L to make an atom for A to stand for. Example uses of atom_chars are as follows:
?- atom_chars(apple, X). X = [a,p,p,l,e] ?- atom_chars(X, [a,p,p,l,e]). X = apple
This predicate is just like atom_chars except that it works with numbers, rather than atoms. Notice that in:
?- atom_chars(X, ['1' ,'2', '3']).
the variable X will be instantiated to the atom '123'. If we want it to be a number instead, we need to use number_chars. Here are some uses of number_chars:
?- number_chars(123.5, X). X = ['1', '2', '3', '.', '5'] ?- number_chars(X, ['1', '2', '3']). X = 123
Recursive Control Loop
The purity of logic programming is undermined by the asserts and retracts of the database. Using these makes code unpredictable, since the behavior of the code depends on global data whose value might change.
Because logical variables cannot have their values changed by assignment, the commands must take two arguments representing the old state and the new state. The repeat-fail control structure will not let us repeatedly change the state in this manner, so we need to write a recursive control structure that recursively sends the new state to itself.
This style of Prolog programming is logically purer, and lends itself to certain types of applications. It also avoids the difficulties often associated with global data. On the other hand, it requires more complexity in dealing with state information in arguments, and the multiple lists and recursive routines can be confusing to debug. You will have to decide which approach to use for each application you write.
There could be serious performance problems with this approach. Prolog uses a stack to keep track of the levels of predicate calls. In the case of a recursive predicate, the stack grows at each recursive call. The stack could easily be consumed in a short period of time by the recursive control structure.
Fortunately, there is a performance feature built into most Prologs that makes these programs behave efficiently.
Tail Recursion
There are actually two kinds of recursive routines. In a true recursive routine, each level must wait for the information from the lower levels in order to return an answer. This means that Prolog must build a stack with a new entry for each level.
This is in contrast to iteration, which is more common in conventional languages. Each pass through the iteration updates the variables and there is no need for building a stack.
There is a type of recursion called tail recursion that, while written recursively, behaves iteratively. In general, if the recursive call is the last call, and there are no computations based on the information from the lower levels, then a good Prolog interpreter can implement the predicate iteratively, without growing the stack.
One classic example of tail recursion is the factorial predicate. First we'll write it using normal recursion. Note that the variable FF, which is returned from the lower level, is used in the top level.
factorial_1 (1,1). factorial_1 (N,F):- N > 1, NN is N - 1, factorial_1 (NN,FF), F is N * FF.
It works as expected.
?- factorial_1 (5,X). x = 120
By introducing a new second argument to keep track of the result so far, we can rewrite factorial tail-recursively. The new argument is initially set to 1. Each recursive call builds on the second argument. When the boundary condition is reached, the third argument is bound to the second argument.
factorial_2(1,F,F). factorial_2(N, T, F) :- N >1, TT is N * T, NN is N - 1, factorial_2(N N, TT, F).
It gives the same results as the previous version, but because the recursive call is the last call in the second clause, its arguments are not needed at each level.
7- factorial_2(5,1,X). x = 120
By introducing a new second argument which will accumulate the partial answer through levels of recursion, we can rewrite reverse. It turns out that the partial answer is already reversed when it reaches the boundary condition.
reverse{[], Rev, Rev).
reverse([HIT], Temp, Rev) :-
reverse(T ,[H ITemp], Rev).
We can now try the second reverse.
?- reverse([ants, mice, zebras], [], X). X = [zebras, mice, ants]
Abstract Data Types and Search
Recursion-Based Graph Search
We next introduce the 3 x 3 knight's tour problem, create a predicate calculus based representation of problem states, and a recursive search of its state space. The chess knight can move on a restricted board as on any regular chessboard: two squares one direction (horizontally or vertically) and one in the other (vertically or horizontally). Thus, the knight can go from square 1 to either square 6 or 8 or from square 9 to either 2 or 4. We ask if the knight can generate a sequence on legal moves from one square to another on this restricted chessboard, from square 1 to 9, for example. The knight's moves are represented in Prolog using move facts.
The path predicate defines an algorithm for a path between its two arguments, the present state, X, and the goal that it wants to achieve, Y. To do this it first tests whether it is where it wants to be, path(Z, Z), and if not, looks for a state, W, to move to.
The Prolog search defined by path is a recursive, depth-first, left-to-right, tree walk. As shown before, assert is a built-in Prolog predicate that always succeeds and has the side effect of placing its argument in the database of specifications. The been predicate is used to record each state as it is visited and then not(been(X)) determines, with each new state found whether that state has been previously visited, thus avoiding looping within the search space.
path(Z, Z). path(X, Y) :- move(X, W), not(been(W)), assert(been(W)), path(W, Y).
This use of the been predicate violates good programming practice in that it uses global side-effects to control search. been(3), when asserted into the database, is a fact available to any other predicate and, as such, has global extension. We created been to modify the program execution.
A more sophisticated method for control of search is to create a list that keeps track of visited states. We create this list and make it the third argument of the path predicate. As each new state is encountered, the member predicate checks whether or not it is already a visited state. If the state is not a member of the list we put it on this list in the order in which it was encountered, the most recent state encountered the head of the list. If the new state is on the list of already visited states, the path predicate backtracks looking for new non-visited states. This approach remedies the problems of using global been(W). The following clauses implement depth-first left-to right graph search with backtracking.
path(Z, Z, L). path(X, Y, L) :- move(X, Z), not(member(Z, L)), path(Z, Y, [Z|L]).
Abstract Data Types in Prolog
Algorithms, Data Structures, and Idioms in Prolog, Lisp, and Java, Pearson 2009?>Programming, in almost any environment, is enhanced by creating procedural abstractions and by hiding information. Because the set, stack, queue, and priority queue data structures are important support constructs for graph search algorithms, a major component of AI problem solving, we build them in Prolog in the present section.>
Since lists, recursion, and pattern matching are the primary tools for building and searching graph structures, they are the pieces with which we build our ADTs. All list handling and recursive processing that define the ADT are hidden
within the ADT abstraction, quite different than the normal static data structure.
The Stack ADT in Prolog
A stack is a linear structure with access at one end only. Thus all elements must be added to, pushed, and removed, popped, from the structure at that access end. The stack is sometimes referred to as a last-in-first-out (LIFO) data structure. We will see its use with depth-first search. The operators that we will define for a stack are:
- 1. Test whether the stack is empty.
- 2. Push an element onto the stack.
- 3. Pop or remove, the top element from the stack.
- 4. Peek (often called Top) to see the top element on the stack without popping it.
- 5. Member_stack, checks whether an element is in the stack.
- 6. Add_list_to stack, adds a list of elements to the stack.
We now build these operators in Prolog, using the list primitives:
-
1. empty_stack([]).
This predicate can be used either to test a stack to see whether it is empty or to generate a new empty stack.
-
2–4. stack(Top, Stack, [Top | Stack]).
This predicate performs the push, pop, and peek predicates depending on the variable bindings of its arguments. For instance, push produces a new stack as the third argument when the first two arguments are bound. Likewise, pop produces the top element of the stack when the third argument is bound to the stack. The second argument will then be bound to the new stack, once the top element is popped. Finally, if we keep the stack as the third argument, the first argument lets us peek at its top element.
-
5. member_stack(Element, Stack) :- member(Element, Stack).
This allows us to determine whether an element is a member of the stack. Of course, the same result could be produced by creating a recursive call that peeked at the next element of the stack and then, if this element did not match Element, popped the stack. This would continue until the empty stack predicate was true.
-
6. add_list_to_stack(List, Stack, Result) :- append(List, Stack, Result).
List is added to Stack to produce Result, a new stack. Of course, the same result could be obtained by popping List (until empty) and pushing each element onto a temporary stack. We would then pop the temporary stack and push each element onto the Stack until empty_stack is true for the temporary stack.
A final predicate for printing a stack in reverse order is reverse_print_stack. This is very useful when a stack has, in reversed order, the current path from the start state to the present state of the graph search.
reverse_print_stack(S) :- empty_stack(S). reverse_print_stack(S) :- stack(E, Rest, S), reverse_print_stack(Rest), write(E), nl.
The Queue ADT in Prolog
A queue is a first-in-first-out (FIFO) data structure. It is often characterized as a list where elements are taken off or dequeued from one end and elements are added to or enqueued at the other end. The queue is used for defining breadth-first search.
The queue operators are:
-
1. empty_queue([]).
This predicate tests whether a queue is empty or initializes a new empty queue.
-
2.
enqueue(E, [ ], [E]). enqueue(E, [H | T], [H | Tnew]) :- enqueue(E, T, Tnew).
This recursive predicate adds the element E to a queue, the second argument. The new augmented queue is the third argument.
-
3. dequeue(E, [E|T], T).
This predicate produces a new queue, the third argument, which is the result of taking the next element, the first argument, off the original queue, the second argument.
-
4. dequeue(E, [E|T], _).
This predicate lets us peek at the next element, E, of the queue.
-
5. member_queue(Element, Queue) :- member(Element, Queue).
This tests whether Element is a member of Queue.
-
6. add_list_to_queue(List, Queue, Newqueue) :- append(Queue, List, Newqueue).
This predicate enqueues an entire list of elements. Of course, 5 and 6 can be created using 1–4.
The Priority Queue ADT in Prolog
A priority queue orders the elements of a regular queue so that each new element added to the priority queue is placed in its sorted order, with the best
element first. The dequeue operator removes the best
sorted element from the priority queue. We will use the priority queue in the design of the best-first search algorithm.
Because the priority queue is a sorted queue, many of its operators are the same as the queue operators, in particular, empty_queue, member_queue, and dequeue (the best
of the sorted elements will be next for the dequeue). enqueue in a priority queue is the insert_pq operator, as each new item is placed in its proper sorted order.
insert_pq(State, [ ], [State]) :- !. insert_pq(State, [H | Tail], [State, H | Tail]) :- precedes(State, H). insert_pq(State, [H | T], [H | Tnew]) :- insert_pq(State, T, Tnew). precedes(X, Y) :- X < Y. % < depends on problem
The first argument of this predicate is the new element that is to be inserted. The second argument is the previous priority queue, and the third argument is the augmented priority queue. The precedes predicate checks that the order of elements is preserved.
Another priority queue operator is insert_list_pq. This predicate is used to merge an unsorted list or set of elements into the priority queue, as is necessary when adding the children of a state to the priority queue for best-first search. insert_list_pq uses insert_pq to put each individual new item into the priority queue:
insert_list_pq([ ], L, L). insert_list_pq([State | Tail], L, New_L) :- insert_pq(State, L, L2), insert_list_pq(Tail, L2, New_L).
The Set ADT in Prolog
A set is a collection of elements with no element repeated. Sets can be used for collecting all the children of a state or for maintaining the set of all states visited while executing a search algorithm.
In Prolog a set of elements, e.g., {a,b}, may be represented as a list, [a,b], with the order of the list not important. The set operators include empty_set, member_set, delete_if_in, and add_if_not_in. We also include the traditional operators for combining and comparing sets, including union, intersection, set_difference, subset, and equal_set.
empty_set([ ]). member_set(E, S) :- member(E, S). delete_if_in_set(E, [ ], [ ]). delete_if_in_set(E, [E | T], T) :- !. delete_if_in_set(E, [H | T], [H | T_new]) :- delete_if_in_set(E, T, T_new), !. add_if_not_in_set(X, S, S) :- member(X, S), !. add_if_not_in_set(X, S, [X | S]). union([ ], S, S). union([H | T], S, S_new) :- union(T, S, S2), add_if_not_in_set(H, S2, S_new),!. subset([], _). subset([H | T], S) :- member_set(H, S), subset(T, S). intersection([], _, []). intersection([H | T], S, [H | S_new]) :- member_set(H, S), intersection(T, S, S_new), !. intersection([_ | T], S, S_new) :- intersection(T, S, S_new), !. set_difference([ ], _, [ ]). set_difference([H | T], S, T_new) :- member_set(H, S), set_difference(T, S, T_new), !. set_difference([H | T], S, [H | T_new]) :- set_difference(T, S, T_new), !. equal_set(S1, S2) :- subset(S1, S2), subset(S2, S1).
we use many of these abstract data types to build more complex graph search algorithms and meta-interpreters in Prolog. For example, the stack and queue ADTs are used to build the open
list that organizes depth-first and breadth-first search. The set ADTs coordinate the closed
list that helps prevent cycles in a search.
Depth-, Breadth-, and Best-First Search Using the Production System Design Pattern
Production System Search in Prolog
The production system is a model of computation that has proved particularly important in AI, both for implementing search algorithms and for modeling human problem solving behavior. A production system provides pattern-directed control of a problem-solving process and consists of a set of production rules, a working memory, and a recognize–act control cycle.
A production system is defined by:
- The set of production rules
- These are often simply called productions. A production is a condition–action pair and defines a single chunk of problem-solving knowledge. The condition part of the rule is a pattern that determines when that rule may be applied by matching data in the working memory. The action part of the rule defines the associated problem-solving step.
- Working memory
- Working memory contains a description of the current state of the world in a reasoning process. This description is a pattern that, in data-driven reasoning, is matched against the condition part of a production to select appropriate problem-solving actions. The actions of production rules are specifically designed to alter the contents of working memory, leading to the next phase of the recognize-act cycle.
- The recognize–act cycle
- The control structure for a production system is simple: working memory is initialized with the beginning problem description. The current state of the problem solving is maintained as a set of patterns in working memory. These patterns are matched against the conditions of the production rules; this produces a subset of the production rules, called the conflict set, whose conditions match the patterns in working memory. One of the productions in the conflict set is then selected (conflict resolution) and the production is fired. After the selected production rule is fired, the control cycle repeats with the modified working memory. The process terminates when the contents of working memory do not match any rule conditions.
- Conflict resolution
- Conflict resolution chooses a rule from the conflict set for firing. Conflict resolution strategies may be simple, such as selecting the first rule whose condition matches the state of the world, or may involve complex rule selection heuristics. The pure production system model has no mechanism for recovering from dead ends in the search; it simply continues until no more productions are enabled and halts. Many practical implementations of production systems allow backtracking to a previous state of working memory in such situations.
Example: The Knight's Tour Revisited
The 3 x 3 knight's tour problem may be solved with a production system. Each move can be represented as a rule whose condition is the location of the knight on a particular square and whose action moves the knight to another square. Sixteen productions represent all possible moves of the knight.
We next specify a recursive procedure to implement a control algorithm for the production system. We will use the recursive path algorithm, where the third argument of the path predicate is the list of already visited states. Because path(Z, Z, L) will unify only with predicates whose first two arguments are identical, such as path(3, 3, _) or path(5, 5, _), it defines the desired terminating condition. If path(X, X, L) does not succeed we look at the production rules for a next state and then recur.
The general recursive path definition is given by two predicate calculus formulas:
path(Z, Z, L). path(X, Y, L) :- move(X, Z), not(member(Z, L)), path(Z, Y, [Z | L]).
Working memory, represented by the parameters of the recursive path predicate, contains both the current board state and the goal state. The control regime applies rules until the current state equals the goal state and then halts. A simple conflict resolution scheme would fire the first rule that did not cause the search to loop. Because the search may lead to dead ends (from which every possible move leads to a previously visited state and thus a loop), the control regime must also allow backtracking.
Production systems are capable of generating infinite loops when searching a state space graph. These loops are particularly difficult to spot in a production system because the rules can fire in any order. That is, looping may appear in the execution of the system, but it cannot easily be found from a syntactic inspection of the rule set.
A Production System Solution to the Farmer, Wolf, Goat, Cabbage Problem*
Designing Alternative Search Strategies
As demonstrated, Prolog itself uses depth-first search with backtracking. We now show how alternative search strategies can be implemented in Prolog. Our implementations of depth-first, breadth-first, and best-first search use open and closed lists to record states in the search. The open list contains all potential next states in the search. How the open list is maintained, as a stack, as a queue, or as a priority queue, determines which particular state is next, that is, search is in either depth-first, breadth-first, or as best-first modes. The closed set keeps track of all the states that have been previously visited, and is used primarily to prevent looping in the graph as well as to keep track of the current path through the space. When search fails at any point we do not backtrack. Instead, open and closed are updated within the path call and the search continues with these revised values. The cut is used to keep Prolog from storing the old versions of the open and closed lists.
Implementing Depth-first Search in Prolog
Because the values of variables are restored when recursion backtracks, the list of visited states in the depth-first path algorithm of the previous section records states only if they are on the current path to the goal. Although testing each new
state for membership in this list prevents loops, it still allows branches of the space to be reexamined if they are reached along paths generated earlier but abandoned at that time as unfruitful. A more efficient implementation keeps track of all the states that have ever been encountered. This more complete collection of states made up the members of the set we call closed, and Closed_set in the following algorithm.
Closed_set holds all states on the current path plus the states that were rejected when the algorithm determined they had no usable children; thus, it no longer represents the path from the start to the current state. To capture this path information, we create the ordered pair [State, Parent] to keep track of each state and its parent; the Start state is represented by [Start, nil]. These state–parent pairs will be used to re-create the solution path from the Closed_set.
We now present a shell structure for depth-first search in Prolog, keeping track of both open and closed and checking each new state to be sure it was not previously visited. path has three arguments, the Open_stack, Closed_set, maintained as a set, and the Goal state. The current state, State, is the next state on the Open_stack. The stack and set operators are discussed here.
Search starts by calling a go predicate that initializes the path call. Note that go places the Start state with the nil parent, [Start, nil], alone on Open_stack; the Closed_set is empty:
go(Start, Goal) :- empty_stack(Empty_open), stack([Start, nil], Empty_open, Open_stack), empty_set(Closed_set), path(Open_stack, Closed_set, Goal).
The three-argument path call is:
path(Open_stack, _, _) :- empty_stack(Open_stack), write(’No solution found with these rules’). path(Open_stack, Closed_set, Goal) :- stack([State, Parent], _, Open_stack), State = Goal, write(`A Solution is Found!’), nl, printsolution([State, Parent], Closed_set). path(Open_stack, Closed_set, Goal) :- stack([State, Parent], Rest_open_stack, Open_stack), get_children(State, Rest_open_stack, Closed_set, Children), add_list_to_stack(Children, Rest_open_stack, New_open_stack), union([[State, Parent]], Closed_set, New_closed_set), path(New_open_stack, New_closed_set, Goal), !. get_children(State, Rest_open_stack, Closed_set, Children) :- bagof(Child, moves(State, Rest_open_stack, Closed_set, Child), Children). moves(State, Rest_open_stack, Closed_set, [Next, State]) :- move(State, Next), not(unsafe(Next)), % test depends on problem not(member_stack([Next,_], Rest_open_stack)), not(member_set([Next,_], Closed_set)).
We assume a set of move rules appropriate to the problem, and, if necessary, an unsafe predicate:
move(Present_state, Next_state) :- ... % test rules move(Present_state, Next_state) :- ... ...
The first path call terminates search when the Open_stack is empty, which means there are no more states on the open list to continue the search. This usually indicates that the graph has been exhaustively searched. The second path call terminates and prints out the solution path when the solution is found. Since the states of the graph search are maintained as [State, Parent] pairs, printsolution will go to the Closed_set and recursively rebuild the solution path. Note that the solution is printed from start to goal.
printsolution([State, nil], _) :- write(State), nl. printsolution([State, Parent], Closed_set) :- member_set([Parent, Grandparent], Closed_set), printsolution([Parent, Grandparent], Closed_set), write(State), nl.
Predicates bagof and moves
The third path call uses bagof, a Prolog built-in predicate standard to most interpreters. bagof lets us gather all the unifications of a pattern into a single list. The second parameter to bagof is the pattern predicate to be matched in the database. The first parameter specifies the components of the second parameter that we wish to collect. For example, we may be interested in the values bound to a single variable of a predicate. All bindings of the first parameter resulting from these matches are collected in a list, the bag, and bound to the third parameter.
In this program, bagof collects the states reached by firing all of the enabled production rules. Of course, this is necessary to gather all descendants of a particular state so that we can add them, in proper order, to open. The second argument of bagof, a new predicate named moves, calls the move predicates to generate all the states that may be reached using the production rules. The arguments to moves are the present state, the open list, the closed set, and a variable that is the state reached by a good move. Before returning this state, moves checks that the new state, Next, is not a member of either rest_open_stack, open once the present state is removed, or closed_set. bagof calls moves and collects all the states that meet these conditions. The third argument of bagof represents the new states that are to be placed on the Open_stack.
For some Prolog interpreters, bagof fails when no matches exist for the second argument and thus the third argument, List, is empty. This can be remedied by substituting (bagof(X, moves(S, T, C, X), List); List = [ ]) for the current calls to bagof in the code.
Finally, because the states of the search are represented as state–parent pairs, member check predicates, e.g., member_set, must be revised to reflect the structure of pattern matching. We test if a state–parent pair is identical to the first element of the list of state–parent pairs and then recur if it isn't:
member_set([State, Parent], [[State, Parent]|_]). member_set(X, [_|T]) :- member_set(X, T).
Implementing Breadth-first Search in Prolog
We now present the shell of an algorithm for breadth-first search using explicit open and closed lists. This algorithm is called by:
go(Start, Goal) :- empty_queue(Empty_open_queue), enqueue([Start, nil], Empty_open_queue, Open_queue), empty_set(Closed_set), path(Open_queue, Closed_set, Goal).
Start and Goal have their obvious values. The shell can be used with the move rules and unsafe predicates for any search problem. Again we create the ordered pair [State, Parent], as we did with depth-first search, to keep track of each state and its parent; the start state is represented by [Start, nil]. This will be used by printsolution to re-create the solution path from the Closed_set. The first parameter of path is the Open_queue, the second is the Closed_set, and the third is the Goal. Don't care variables, those whose values are not used in a clause, are written as _
.
path(Open_queue, _, _) :- empty_queue(Open_queue), write(’Graph searched, no solution found.’). path(Open_queue, Closed_set, Goal) :- dequeue([State, Parent], Open_queue, _), State = Goal, write(’Solution path is: ‘), nl, printsolution([State, Parent], Closed_set). path(Open_queue, Closed_set, Goal) :- dequeue([State, Parent], Open_queue, Rest_open_queue), get_children(State, Rest_open_queue, Closed_set, Children), add_list_to_queue(Children, Rest_open_queue, New_open_queue), union([[State, Parent]], Closed_set, New_closed_set), path(New_open_queue, New_closed_set, Goal), !. get_children(State, Rest_open_queue, Closed_set, Children) :- bagof(Child, moves(State, Rest_open_queue, Closed_set, Child), Children). moves(State, Rest_open_queue, Closed_set, [Next, State]) :- move(State, Next), not(unsafe(Next)), %test depends on problem not(member_queue([Next,_], Rest_open_queue)), not(member_set([Next,_], Closed_set)).
This algorithm is a shell in that no move rules are given. These must be supplied to fit the specific problem domain, such as the FWGC problem. The queue and set operators have been described here.
The first path termination condition is defined for the case that path is called with its first argument, Open_queue, empty. This happens only when no more states in the graph remain to be searched and the solution has not been found. A solution is found in the second path predicate when the head of the open_queue and the Goal state are identical. When path does not terminate, the third call, with bagof and moves predicates, gathers all the children of the current state and maintains the queue. (The detailed actions of these two predicates have been described here.) In order to recreate the solution path, we saved each state as a state–parent pair, [State, Parent]. The start state has the parent nil. As already noted, the state–parent pair representation makes necessary a slightly more complex pattern matching in the member, moves, and printsolution predicates.
Implementing Best-first Search in Prolog
Our shell for best-first search is a modification of the breadth-first algorithm in which the open queue is replaced by a priority queue, ordered by heuristic merit, which supplies the current state for each new call to path. In our algorithm, we attach a heuristic measure permanently to each new state on open and use this measure for ordering the states on open. We also retain the parent of each state. This information is used by printsolution, as in depth- and breadth-first search, to build the solution path once the goal is found.
To keep track of all required search information, each state is represented as a list of five elements: the state description, the parent of the state, an integer giving the depth in the graph of the state's discovery, an integer giving the heuristic measure of the state, and the integer sum of the third and fourth elements. The first and second elements are found in the usual way; the third is determined by adding one to the depth of its parent; the fourth is determined by the heuristic measure of the particular problem. The fifth element, used for ordering the states on the open_pq, is f(n) = g(n) + h(n). A justification for using this approach to order states for heuristic search, usually referred to as the A Algorithm, is presented in Luger (2009, Chapter 4).
As before, the move rules are not specified; they are defined to fit the specific problem. The ADT operators for set and priority queue are presented in the section on ADTs. heuristic, also specific to each problem, is a measure applied to each state to determine its heuristic weight, the value of the fourth parameter in its descriptive list.
This best-first search algorithm has two termination conditions and is called by:
go(Start, Goal) :- empty_set(Closed_set), empty_pq(Open), heuristic(Start, Goal, H), insert_pq([Start, nil, 0, H, H], Open, Open_pq), path(Open_pq, Closed_set, Goal).
nil is the parent of Start and H is its heuristic evaluation. The code for best-first search is:
path(Open_pq, _,_) :- empty_pq(Open_pq), write(’Graph searched, no solution found.’). path(Open_pq, Closed_set, Goal) :- dequeue_pq([State, Parent, _, _, _], Open_pq,_), State = Goal, write(’The solution path is: ‘), nl, printsolution([State, Parent, _, _, _], Closed_set). path(Open_pq, Closed_set, Goal) :- dequeue_pq([State, Parent, D, H, S], Open_pq, Rest_open_pq), get_children([State, Parent, D, H, S], Rest_open_pq, Closed_set, Children, Goal), insert_list_pq(Children, Rest_open_pq, New_open_pq), union([[State, Parent, D, H, S]], Closed_set, New_closed_set), path(New_open_pq, New_closed_set, Goal), !.
get_children is a predicate that generates all the children of State. It uses bagof and moves predicates as in the previous searches, with details earlier this section. A set of move rules, a safe check for legal moves, and a heuristic must be specifically defined for each application. The member check must be specifically designed for five element lists.
get_children([State,_,D,_, _],Rest_open_pq, Closed_set,Children,Goal) :- bagof(Child, moves([State, _, D, _, _], Rest_open_pq, Closed_set, Child,Goal), Children). moves([State, _, Depth, _, _], Rest_open_pq, Closed_set,[Next,State,New_D,H,S], Goal) :- move(State, Next), not(unsafe(Next)), % application specific not(member_pq([Next, _, _, _, _],Rest_open_pq)), not(member_set([Next, _, _, _, _],Closed_set)), New_D is Depth + 1, heuristic(Next, Goal, H), % application specific S is New_D + H.
printsolution prints the solution path, recursively finding state–parent pairs by matching the first two elements in the state description with the first two elements of the five element lists that make up the Closed_set.
printsolution([State, nil, _, _, _], _) :- write(State), nl. printsolution([State, Parent, _, _, _], Closed_set):- member_set([Parent, Grandparent, _, _, _], Closed_set), printsolution([Parent, Grandparent, _, _, _], Closed_set), write(State), nl.
Special or Advanced Topics in Prolog
The Effect of Rule and Goal Ordering
Two syntactic issues, irrelevant for logic programs, are important to consider when composing Prolog programs. The rule order, or clause order, of clauses in each procedure must be decided. Also the goal order of goals in the bodies of each clause must be determined. The consequences of these decisions can be immense. There can be orders of magnitude of difference in efficiency in the performance of Prolog programs. In extreme though quite common cases, correct logic programs will fail to give solutions because of nontermination.
The rule order determines the order in which solutions are found.
Changing the order of rules in a procedure permutes the branches in any search tree for a goal using that procedure. The search tree is traversed depth-first. So permuting the branches causes a different order of traversal of the search tree, and a different order of finding solutions.
The Effect of Goal Order in Prolog
Goal order is more significant than clause order. It is the principal means of specifying sequential flow of control in Prolog programs.
We first discuss goal order from the perspective of database programming. The reason that the order of goals in the body of a clause affects the order of solutions to a query is different from the reason that the order of rules in a procedure affects the solution order. Changing rule order does not change the search tree that must be traversed for a given query. The tree is just traversed in a different order. Changing goal order changes the search tree.
Goal order determines the search tree.
Goal order affects the amount of searching the program does in solving a query by determining which search tree is traversed.
The optimal goal order of Prolog programs varies with different uses. Consider the definition of grandparent. There are two possible rules:
grandparent(X,Z) :- parent(X,Y), parent(Y,Z), grandparent(X,Z) :- parent(Y,Z), parent(X,Y).
If you wish to find someone's grandson with the grandfather/2 relationship with a query such as grandparent (abraham, X)?, the first of the rules searches more directly. If looking for someone's grandparent with a query such as grandparent (X, isaac) ?, the second rule finds the solution more directly. If efficiency is important, then it is advisable to have two distinct relationships, grandparent and grandchild, to be used appropriately at the user's discretion.
In contrast to rule order, goal order can determine whether computations terminate. Consider the recursive rule for ancestor:
ancestor(X,Y) :- parent(X, Z), ancestor(Z,Y).
If the goals in the body are swapped, the ancestor program becomes left recursive, and all Prolog computations with ancestor are nonterminating.
Non-Termination in Prolog
Nontermination arises with recursive rules. Consider a friend/2 predicate. A sample fact would be friend(fred,tom). Since this relationship is commutative, we might be tempted to add a rule like friend(X,Y) :- friend(Y,X).. But if we do so, then no computation involving friend/2 would ever terminate.
Recursive rules that have the recursive goal as the first goal in the body are known as left recursive rules. The foregoing rule is an example. Left recursive rules are inherently troublesome in Prolog. They cause nonterminating computations if called with inappropriate arguments.
The best solution to the problem of left recursion is avoidance. Commutative relationships are best handled differently, by defining a new predicate that has a clause for each permutation of the arguments of the relationship. We might define the relationship friend/2 as a rule whose goals are made up of befriend predicate, the one used in stating facts about friendship. Thus:
befriend(fred, tom). friend(X,Y) :- befriend(X,Y). friend(X,Y) :- befriend(Y,X).
Unfortunately, it is not generally possible to remove all occurrences of left recursion...
Binding to C
The nature of this interaction with Prolog dictates the nature of a C to Prolog interface. It too must be able to either execute compiled Prolog code, or query a loaded Prolog program. In this sense, the interface from C to Prolog will look more like a database API than procedural interlanguage calls.
The Hello World program illustrates the Prolog to C direction of the interface as well. Note that the write statement has nothing to do with logic, pattern- matching, or search; it simply performs I/O. Prolog provides a number of special predicates, such as write, which are used primarily for their side effects. It is in this area that Prolog is weaker than C.
The Prolog programmer must rely on whatever special predicates a particular vendor provides with an implementation. So, for example, if a particular Prolog implementation doesn't supply the tools for accessing Windows, then it can't be used to implement Windows applications. This is where Prolog to C connections come in. They let a programmer define as many extended predicates, such as write, as desired, to allow Prolog code access to any services accessable from C. This fundamental architecture of Prolog shapes the design of an interface between it and C. The calls from C to Prolog reflect the database nature of Prolog. The calls from Prolog to C reflect the procedural nature of C.
Prolog Applications
Expert Systems in Prolog
Asking the User
The ask predicate will have to determine from the user whether or not a given attribute-value pair is true. The program needs to be modified to specify which attributes are askable. This is easily done by making rules for those attributes that call ask.
eats(X):- ask(eats, X). feet(X):- ask(feet, X). wings(X):- ask(wings, X). neck(X):- ask(neck, X). color(X):- ask(color, X).
Now if the system has the goal of finding color(white) it will call ask, rather than look in the program. If ask(color, white) succeeds, color(white) succeeds.
The simplest version of ask prompts the user with the requested attribute and value and seeks confirmation or denial of the proposed information. The code is:
ask(Attr, Val):-
write(Attr:Val),
write('? '),
read(yes).
The read will succeed if the user answers yes, and fail if the user types anything else. Now the program can be run without having the data built into the program. The same query to bird starts the program, but now the user is responsible for determining whether some of the attribute-values are true. The following dialog shows how the system runs:
?- bird(X). nostrils : external_tubular? yes. live : at_sea? yes. bill : hooked? yes. size : large? yes. wings : long_narrow? yes. color : white? yes. X = laysan_albatross
There is a problem with this approach. If the user answered no to the last question, then the rule for bird(laysan_albatross) would have failed and backtracking would have caused the next rule for bird(black_footed_albatross) to be tried. The first subgoal of the new rule causes Prolog to try to prove family(albatross) again, and ask the same questions it already asked. It would be better if the system remembered the answers to questions and did not ask again.
Remembering the Answer
A new predicate, known/3 is used to remember the user's answers to questions. It is not specified directly in the program, but rather is dynamically asserted whenever ask gets new information from the user.
Every time ask is called it first checks to see if the answer is already known to be yes or no. If it is not already known, then ask will assert it after it gets a response from the user. The three arguments to known are: yes/no, attribute, and value. The new version of ask looks like:
ask(A, V):-
known(yes, A, V), % succeed if true
!. % stop looking
ask(A, V):-
known(_, A, V), % fail if false
!,
fail.
ask(A, V):-
write(A:V), % ask user
write('? : '),
read(Y), % get the answer
asserta(known(Y, A, V)), % remember it
Y == yes. % succeed or fail
The cuts in the first two rules prevent ask from backtracking after it has already determined the answer.
Multi-valued Answers
There is another level of subtlety in the approach to known. The ask predicate now assumes that each particular attribute value pair is either true or false. This means that the user could respond with a yes
to both color:white and color:black. In effect, we are letting the attributes be multi-valued. This might make sense for some attributes such as voice but not others such as bill, which only take a single value.
The best way to handle this is to add an additional predicate to the program, which specifies the attributes that are multi-valued:
multivalued(voice). multivalued(feed).
A new clause is now added to ask to cover the case where the attribute is not multi-valued (and therefore single-valued) and already has a different value from the one asked for. In this case ask should fail. For example, if the user has already answered yes to size - large then ask should automatically fail a request for size - small without asking the user. The new clause goes before the clause which actually asks the user:
ask(A, V):- not multivalued(A), known(yes, A, V2), V \== V2, !, fail.
Menus for the User
The user interface can further be improved by adding a menu capability that gives the user a list of possible values for an attribute. It can further enforce that the user enter a value on the menu.
This can be implemented with a new predicate, menuask. It is similar to ask, but has an additional argument which contains a list of possible values for the attribute. It would be used in the program in an analogous fashion to ask:
size(X):- menuask(size, X, [large, plump, medium, small]). flight(X):- menuask(flight, X, [ponderous, agile, flap_glide]).
The menuask predicate can be implemented using either a sophisticated windowing interface, or by simply listing the menu choices on the screen for the user. When the user returns a value it can be verified, and the user reprompted if it is not a legal value.
A simple implementation would have initial clauses as in ask, and have a slightly different clause for actually asking the user. That last clause of menuask might look like:
menuask(A, V, MenuList) :-
write('What is the value for'), write(A), write('?'), nl,
write(MenuList), nl,
read(X),
check_val(X, A, V, MenuList),
asserta( known(yes, A, X) ),
X == V.
check_val(X, A, V, MenuList) :-
member(X, MenuList),
!.
check_val(X, A, V, MenuList) :-
write(X), write(' is not a legal value, try again.'), nl,
menuask(A, V, MenuList).
The check_val predicate validates the user's input. In this case the test ensures the user entered a value on the list. If not, it retries the menuask predicate.
Biology in Prolog
Our First Database of Biology in Prolog
First belong/2 is defined. It will be used instead of member/2. Then some discontiguous predicates are registered. Last, predicates has/2 and isa/2 are defined. Now we are ready to present some biological facts!
Electronics in Prolog
Combinational Circuits in Prolog
Let us first work out how to represent logic gates as predicates. Such predicates shall reflect their truth tables...
inv(0,1). inv(1, 0). nand(O, 0, 1). nand(O, 1, 1). nand(1, 0, 1). nand(1, 1,0).
Circuits may be built up by constructing Prolog procedures containing goals for representing circuit elements. Consider a circuit represented by:
Let's call the output of the incoming nand and nor gates t
circ1(A, B, C, D, E) :- nand(A, B, T1), nor(C, 0, T2), nand(T1, T2, E).
Inputs and internal nodes may be shared simply by naming them the same.