programming languages and paradigms 6.1 characteristics of ...blk/cs3490/ch06/ch06.01.02.pdf ·...

23
Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris 6.1 Characteristics of Logical Programming Languages Chapter 3 examined imperative languages, whose design is an abstraction of the underlying von Neumann architecture. Functional programming languages were discussed in chapters 4 and 5. The design of functional languages is distinct from the underlying architecture and thus, programmers are spared much of the concerns of memory management and data representation. This chapter explores logic programming languages, arguably the most high level of the general purpose high-level programming languages. Programming in a logic or declarative language involves developing a set of declarations, or statements of truth, that are used by the language interpreter to derive additional truths. Logic programming languages are vastly different from languages in other paradigms because the programmer provides statements about the domain under consideration and the language interpreter, rather than the programmer, determines how these statements are used to prove or disprove goals. In other words, the programmer does not express a flow of control in the logic program, as is done in other language paradigms. However, the programmer does need a sophisticated understanding about how the language interpreter uses the statements in the program to solve the problem in order to program in the language effectively. Logic languages, such as Prolog, are based on first-order predicate calculus also called first-order logic. Predicate calculus provides a syntax for expressing propositions, which are statements that may be either true or false, and a method for inferring new propositions from the given propositions. Initial investigations into logic programming were question-answering systems where logic was used to represent information and some type of inferencing process answered questions based upon the information [John McCarthy’s advice taker paper]. However, Prolog, the most widely used logic programming language, has been used to implement a wide variety of applications. A comprehensive study of first-order predicate calculus is beyond the scope of this book. However, an informal overview should provide an understanding of the use of predicate calculus in the design of logic languages. Thus, briefly, the syntax of predicate calculus includes the following Predicates (Relations) used to express a property about the terms in the language represented by the predicate calculus. For example, father(jay, baron) could express that Jay is the father of Baron. Functions applied to terms to represent another term in the language; for example, father-of(baron) represents the father of Baron. Note that this isn’t a function in the way that is typical for programming languages. father-of(baron) does not yield jay; rather, father-of(baron) is a term that represents the father of baron. Terms - Terms are arguments of predicates and functors. Terms can be: o Variables typically represented by the lower case letters x, y, z

Upload: others

Post on 03-Jun-2020

18 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Programming Languages and Paradigms 6.1 Characteristics of ...blk/cs3490/ch06/ch06.01.02.pdf · Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris 6.1 Characteristics

Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris

6.1 Characteristics of Logical Programming Languages Chapter 3 examined imperative languages, whose design is an abstraction of the underlying von Neumann architecture. Functional programming languages were discussed in chapters 4 and 5. The design of functional languages is distinct from the underlying architecture and thus, programmers are spared much of the concerns of memory management and data representation. This chapter explores logic programming languages, arguably the most high level of the general purpose high-level programming languages. Programming in a logic or declarative language involves developing a set of declarations, or statements of truth, that are used by the language interpreter to derive additional truths. Logic programming languages are vastly different from languages in other paradigms because the programmer provides statements about the domain under consideration and the language interpreter, rather than the programmer, determines how these statements are used to prove or disprove goals. In other words, the programmer does not express a flow of control in the logic program, as is done in other language paradigms. However, the programmer does need a sophisticated understanding about how the language interpreter uses the statements in the program to solve the problem in order to program in the language effectively. Logic languages, such as Prolog, are based on first-order predicate calculus also called first-order logic. Predicate calculus provides a syntax for expressing propositions, which are statements that may be either true or false, and a method for inferring new propositions from the given propositions. Initial investigations into logic programming were question-answering systems where logic was used to represent information and some type of inferencing process answered questions based upon the information [John McCarthy’s advice taker paper]. However, Prolog, the most widely used logic programming language, has been used to implement a wide variety of applications. A comprehensive study of first-order predicate calculus is beyond the scope of this book. However, an informal overview should provide an understanding of the use of predicate calculus in the design of logic languages. Thus, briefly, the syntax of predicate calculus includes the following Predicates (Relations) – used to express a property about the terms in the

language represented by the predicate calculus. For example,

father(jay, baron) could express that Jay is the father of Baron.

Functions – applied to terms to represent another term in the language; for

example, father-of(baron) represents the father of Baron. Note that

this isn’t a function in the way that is typical for programming languages.

father-of(baron) does not yield jay; rather, father-of(baron) is

a term that represents the father of baron. Terms - Terms are arguments of predicates and functors. Terms can be:

o Variables – typically represented by the lower case letters x, y, z

Page 2: Programming Languages and Paradigms 6.1 Characteristics of ...blk/cs3490/ch06/ch06.01.02.pdf · Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris 6.1 Characteristics

Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris

o Constants - represent objects in the language such as jay and baron

o Function applications – represent objects in the language (as described earlier)

Connectives – are used to connect atomic formulas to create a compound formulas

o (conjunction) – used to express that a compound formula is true if the two formulas connected by the conjunction are true; for

example, father(jay, baron) father(jay, grayson)

o (disjunction) - used to express that a compound formula is true if either of the two formulas connected by the disjunction are true; for

example, atwork(cindy) athome(cindy)

o (negation) – used to express the negation of a formula; for

example, father(cindy, baron)

o (implication) – used to express a conditional (if/then) formula; for

example, father(jay, baron) father(jay, grayson)

sibling(baron, grayson)

o (biconditional) – used to express bidirectional implication; for

example, father(jay, baron) son(baron, jay)

Quantifiers,

o (universal quantifier) - predicates and connectives are used to express sentences (also called formulas) in a language; the universal quantifier can be used to assert that the sentence is true

for all values of a variable; for example, (x) (y) (father(x,

y) son(y, x))

o (existential quantifier) – can be used to assert that a sentence is

true for at least one value of a variable; for example, (x) father(x, baron)

A deductive system is used to determine formulae that are logical consequences of other formulae. There are many deductive systems for first-order logic; the system employed in Prolog systems is based upon a technique known as resolution. The idea behind resolution is fairly simple. Assume the language consists of two formulae in the following form:

P Q

Q R

These meaning of the first formula is if P then Q follows. Similarly, if Q is true the R follows. Resolution uses these two formulae to infer another formula:

P R

Automating the process of resolution requires that the formulae are in a standard form known as clausal normal form. In clausal normal form, all quantifiers are eliminated and all formulae are either in the form: C

Page 3: Programming Languages and Paradigms 6.1 Characteristics of ...blk/cs3490/ch06/ch06.01.02.pdf · Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris 6.1 Characteristics

Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris

a formula with no connectives that represents a fact such as father(jay, baron) or in the form:

A1 A2 … An B

such as father(x, y) father(x, z) ^ equal(y, z) sibling(y, z). The resolution process requires performing matching between goal formulae and known formulae and determining values for variables that cause these formulae to match. Determining values for variables is called unification. For example,

the formula fruit(x) matches fruit(apple) for x equal to apple.

Continuing the resolution process requires using the values identified by the unification process. The temporary assignment of these values to variables is known as instantiation. At some point the resolution process may fail to find a matching between a goal formula and known formulae, at which point the resolution back tracks and tries another route. For example, given the set of

formula below, when attempting to identify y and z for which sibling(y, z) is

true, x and y may be instantiated to charlie and bradley. This will cause the

resolution to fail because it can not find a value for z; backtracking will occur and

the process will continue with new values for x and y. father(charlie, bradley)

father(jay, baron)

father(jay, grayson)

father(x, y) father(x, z) ^ equal(y, z) sibling(y, z)

How resolution is performed in Prolog is explained in more detail in the next section. The use of logic languages offers several important advantages. First, the design of the language allows the programmer to focus on the logic required and not on the how the problem is solved. This enables the programmer to concentrate on the problem without being concerned about the details of the underlying target architecture such as memory management, data representation, or how the statements are translated into equivalent machine instructions. In fact, a logic programming language is a great “thinking tool” as it forces the programmer to discover the interrelationships of the problem domain. Formalizing one’s knowledge as is required by implementing a logic program can be a painstaking task. However, the process can yield additional insight into the problem yielding a solution that has a better chance of being correct. This rarely is the case when programming in an imperative programming language. In addition, the high level nature of the language makes it easier to express complex ideas. A logic language can be suitable tool for rapid prototyping. Using a logic language, the programmer needs only to focus on the problem logic without having to match the logic to an imperative style. In addition, the incorporation of new relationships in the problem domain, as well as changes in the problem solution, can be made quickly in a logic program.

Page 4: Programming Languages and Paradigms 6.1 Characteristics of ...blk/cs3490/ch06/ch06.01.02.pdf · Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris 6.1 Characteristics

Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris

Logic languages are particularly well suited for finding a solution to a problem that can be specified logically. For example, an expert system is a type of application that requires the representation of the knowledge base of human expertise. Expert systems have been developed supporting tasks in various fields including medicine, manufacturing, control systems, and financial services. Logic languages are also suitable for systems that process natural language. An important example is the IBM Watson question answering system used to defeat two Jeopardy champions, Ken Jennings and Brad Rutter. The Watson system used Prolog to parse the sentences provided by the game and determine elements of the sentence and relationships between the elements. Finally, Prolog has also been found to be a useful in game development for handling natural language and expressing game logic. The remainder of this chapter provides an introduction to programming in Prolog and discusses the fundamental issues related to the Prolog programming language. 6.2 Programming in Prolog The programs discussed in this chapter were developed using GNU Prolog, a free Prolog compiler available from www.gProlog.org. Other versions of Prolog may vary in the built-in functions available and how some fundamental design decisions, such as negation as failure, are handled. 6.2.1 Facts and Simple Rules Our first example of programming using Prolog involves the representation of family relationships. Here is a pictorial representation of the top level of a family tree: Douglas is married to Rita (indicated by -------) and have two children, Rosa and Gregory, indicated by the solid lines down to the next generation.

There are a variety of ways to represent this data as a set of facts in Prolog. We present one way and in an exercise you will consider alternative representations. Our representation will consist of two attributes: the gender of the person, male or female, and a parent relationship between two people. Here are the facts for the above relationships: male(douglas).

male(gregory).

female(rita).

female(rosa).

Page 5: Programming Languages and Paradigms 6.1 Characteristics of ...blk/cs3490/ch06/ch06.01.02.pdf · Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris 6.1 Characteristics

Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris

parent(rosa, douglas).

parent(rosa, rita).

parent(gregory, douglas).

parent(gregory, rita).

This is actual Prolog code that you can enter into a source code file; call it “family.pl”. Literal or constant names in Prolog start with a lower case letter, so

both the fact names, male, female, and parent, and the fact data, douglas

and so forth, begin with a lower case letter. The ordering of the parent relationship must be uniform; as shown above the relation is parent(<name of child>, <name of parent>). The ordering could have been reversed, but once the relation is established it must be used consistently throughout the application. Enter the data shown above in the file “family.pl” then run the Prolog interpreter. If your file ends in .pl and the file name contains no special characters, you can load it into the Prolog interpreter as shown here: ?-[family].

This is an abbreviated form of the general rule: ?-consult(‘family.pl’).

Either form is a query and must be ended with a period (.). If you have typed in the facts correctly you should receive a reply indicating the file loaded correctly. You can ask the Prolog inference engine to test whether a given fact is true or false. ?-male(gregory).

would return yes meaning it is true. However ?-parent(douglas, rosa).

would return no since this is not a known fact and cannot be derived based on the current set of facts and rules. We can introduce variables in a Prolog query by using an initial upper case letter in the name. The Prolog inference engine will attempt to find a substitution for the variable that will make the query true. Several substitutions may be possible and you can request additional substitutions by typing a semicolon. For example: ?-female(Woman).

Woman = rita; % request another solution

Woman = rosa; % request another solution

no % there are no more solutions

If you put more than one variable in a query, then Prolog will try to find a binding for all the variables that will make the query true. For example: ?-parent(Child, Parent).

Child = rosa

Parent = douglas

If you enter a return, Prolog will return to the query prompt and not produce any additional answers. For this query there were a total of four possible answers. Exercise: Here is a more complete version of the family tree:

Page 6: Programming Languages and Paradigms 6.1 Characteristics of ...blk/cs3490/ch06/ch06.01.02.pdf · Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris 6.1 Characteristics

Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris

Enter these facts into the “family.pl” file and test your data by making many queries to convince yourself that the data is entered correctly. Exercise: Consider some alternative ways to represent the family tree data and rules about relationships. For example, suppose the raw data included a married

fact, such as married(douglas,rita). Give one advantage and one

disadvantage of including these additional facts (but do not enter these facts into the database). The family tree given above has many different relationships beyond the parent relationship and gender relationship given as facts. There are brothers, sisters, aunts, uncles, cousins, grandparents, and so forth. This data could be entered as new facts, but this would overlook the true power of Prolog where you can write rules to allow the system to discover these relationships as a result of logical rules. To make this concrete, let’s write a rule that discovers if a person is a mother. A mother is any female parent, so the rule is very simple: mother(C, M):- parent(C, M), female(M).

This rule says M is the mother of child C “if” M is a parent of C “and” M is a female. Conceptually you can think of :- as “if” and the , as “and”. Consider the query: ?-mother(mary, X).

X = janet; % request another solution

no % there is none

Page 7: Programming Languages and Paradigms 6.1 Characteristics of ...blk/cs3490/ch06/ch06.01.02.pdf · Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris 6.1 Characteristics

Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris

Although you are encouraged to use meaningful literal names and variable names, the Prolog system attaches no meaning to the names themselves. Consider: ?-mother(mary, Father).

Father = janet

Exercise: Enter the “mother” rule into your family.pl file. Write a similar rule for father. Test both of your rules with the given family tree. Not all rules are as simple as the mother and father rules. Consider writing a sibling rule. We can all agree if person X and person Y have the same mother and the same father, then they should be siblings. Here is this rule stated in Prolog: sibling(X,Y):-

mother(X,M),mother(Y,M),father(X,F),father(Y,F).

Notice that M and F only appear in the conditional part of the query. Once a variable is bound to a value, then it must bind to the same value for the entire scope of the rule. So once the mother of X is determined, it binds M to a particular value and for mother(Y,M) to succeed, Y must have the same mother as X. Exercise: Enter the sibling rule into your family.pl file and then test it. Make sure you request multiple solutions. You will soon discover that this rule does not work as expected. See if you can explain what the problem is. Consider the query: ?-sibling(james, X). % you expect the answer rosa

X = james % can james be a sibling to himself?

The rule as written (same mother and same father) would give this unexpected result. We must modify the rule to add one more condition, X and Y cannot bind to the same value. Here is the modified rule: sibling(X,Y):-mother(X,M),mother(Y,M),father(X,F),

father(Y,F),X=/=Y.

Enter this rule into your family.pl file and test it. Try the query: ?- sibling(X,Y). % request multiple solutions

At some point in the possible solutions you should find X=james and Y=rosa but you would also find X=rosa and Y=james. Should this be considered an error to have the “same” solution appear twice? Since we are not attaching any meaning to the position of the arguments in the sibling rule and since being a sibling is a symmetric relationship, then discovering these two separate answers should be considered correct. Exercise: Once you have the sibling rule working as expected, use it to write two more rules for brother and sister. The query brother(rosa,B) should succeed with B = james and this should be the only solution. Exercise: Use the sibling rule, the parent rule, and the facts about gender to write an aunt rule and an uncle rule. Here is an example of the aunt rule:

Page 8: Programming Languages and Paradigms 6.1 Characteristics of ...blk/cs3490/ch06/ch06.01.02.pdf · Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris 6.1 Characteristics

Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris

?-aunt(jason,A).

A = rosa

We now want to write a cousin rule. We could say X and Y are cousins if a parent of X, call this person Px, and a parent of Y, call this person Py, are siblings. Here is one way to write this rule: cousin(X,Y):- parent(X,Px), parent(Y,Py), sibling(Px,Py).

Enter this rule into your family.pl file and test it out. Notice that cousin is a symmetric relationship. Here is a grandparent rule: grandparent(X,GP):- parent(X,P), parent(P,GP).

Enter this rule into your file and then complete the exercise below. Exercise: Write an alternative rule for cousin, name it cousin1, that is based on the statement X and Y are cousins if they have the same grandparents. Be careful in writing this rule so that you cannot be a cousin to yourself or to your siblings. As our final example of writing rules to discover a relationship we will extend the idea behind the grandfather rule to a more general rule to find any ancestor. If we wrote a greatgrandparent rule it would require a chain of three parent relationships. To write a rule for any ancestor we will need recursion to apply the parent relationship as many times as needed to reach the ancestor. Just like recursion for imperative programming and functional programming we will have to specify a base case and a general recursive case. The base case is: ancestor(X,Y):- parent(X,Y).

The recursive case is used to climb up one parent relationship and then use recursion to climb the remaining parent relations. Here is the rule: ancestor(X,Y):- parent(X,P), ancestor(P,Y).

When there are multiple rules with the same name and arity (that is, number of argument) the rules are tried in a top down fashion. Therefore, when entering these rules in the file, we will enter the base case rule first then the recursive case second. Enter these rules into your file and test them out. Exercise: Without using the ancestor rule, write a descendant that uses a base case and a recursive case. Similar to functional programming, the primary mechanism for repetition in Prolog is the use of recursion. We explore this in detail in the next section when we learn how to process lists. 6.2.2 List Structures The most fundamental data structure in Prolog is the list. Lists are enclosed in square brackets, […], and the items in a list are separated by commas. Here is an example of a list of literal values: [bear, tiger, lion, giraffe, elephant]

Page 9: Programming Languages and Paradigms 6.1 Characteristics of ...blk/cs3490/ch06/ch06.01.02.pdf · Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris 6.1 Characteristics

Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris

When we write rules to process a list structure, we use variables in the rules to bind values using the unification process. A vertical bar in a list separates items at the head of the list from the tail of the list. For example, when the list [Head | Tail]

unifies with the list of animals given above, then Head = bear and Tail =

[tiger, lion, giraffe, elephant]. You can have multiple items at the

head of the list separated by commas. So [Animal1, Animal2 |

RestOfAnimals] binds Animal1 = bear, Animal2 = tiger and

RestOfAnimals = [lion, giraffe, elephant]. Notice that the variable

after the vertical bar will be bound to a list structure. We now want to write our first rule to process a list and return the last item in the list. As an example use of the rule consider ?-last([bear, tiger, lion, giraffe, elephant], LastItem).

LastItem = elephant

The first argument is the list to be processed and the second argument is bound to the last item in that list. This will be a recursive rule with a base case, a list that only contains a single item, and the general case where the list contains multiple items. In the latter case we remove the first item from the list and recurse on the rest of the list looking for the last item. Here is a first version of our rule: % base case, the single item in the list is the last item

last([X], X).

% general case, discard first item, search the rest of the

list

last([Head|Tail], X) :- last(Tail, X).

Start a file called ‘lists.pl’ where you will collect together the rules for processing lists we will develop in this section. Type in the two rules for last shown above; put the base case first. Load this into the Prolog interpreter and test it. If your version of Prolog says something like “cannot redefine a built-in rule” then this means last has already been defined by the system; simply change the name of the rule to lastItem and this warning will go away. Include the empty list in your tests; what happened? When using gProlog and loading this file you will get a “singleton variable” warning on the second part of the rule. The variables Tail and X appear twice in the rule, so these are not the cause of the warning; the variable Head only appears once and this is the cause of the warning. Head will be bound to a value but will never be used elsewhere in the rule. This is inefficient; the warning can be eliminate by using an underscore (_), a “don’t care” value, in place of Head. When you make the change shown below, the warning should go away. last([_|Tail], X) :- last(Tail, X).

Languages in the imperative, functional, and object-oriented paradigms distinguish input values to a function from the return value. Prolog makes no such distinction. It is possible to specify the “output” value and ask Prolog to

Page 10: Programming Languages and Paradigms 6.1 Characteristics of ...blk/cs3490/ch06/ch06.01.02.pdf · Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris 6.1 Characteristics

Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris

generate the input that would produce that output. Consider our last rule; we can make the following query: ?-last(List, world).

List = [world];

List = [_, world];

List = [_, _, world]

Prolog produces lists of increasing length, each ending in world with “don’t care”

values before that. Our next rule will append two lists together to produce a third list; we will call it app to avoid any conflict with built-in rules. Here is how the rule works: ?-app([1,2,3],[a,b,c,d], L).

L = [1,2,3,a,b,c,d]

Prolog is not a strongly typed language and does not require items in a list to be the same type. When writing Prolog rules for you need to ask two questions: (1) what is the base case, it does not require recursion (2) how can the general case be “forced towards” the base case When appending two lists together, if the first list is empty then the result is the second list. We express this as the base case: app([ ], L2, L2).

Now we have to think how to force the general case towards the base case

where the first list is empty. We know from the last rule we forced a list to

become empty by removing the head item; we apply the same philosophy here but in this case we can’t throw away the head item. Rather we add the head item as the first item in the result list from the recursive call. Here is the general case: app([Head| Tail], L2, [Head | Result]):- app(Tail, L2,

Result).

Notice that each recursive call makes the first list one item smaller, so we will eventually reach the base case of the empty first list. When the base case is reached, the result is the second list, L2 above. As we back out of recursion, items are added from the first list to the result list. Once the recursive call is completed, the result list is the first list concatenated onto the second list. Exercise: Enter the app rule into your lists.pl file and test it in the Prolog interpreter. When you test your rules, make sure you include limiting cases, such as (1) the first list is empty, (2) the second list is empty, (3) both the first and second lists are empty. Exercise: Test to see if your app rule can “run backwards” in the sense you provide the result and Prolog provides the inputs that could produce the desired result. For example, consider the query: ?-app(L1, L2, [a,b,c,d]).

L1 = [ ]

L2 = [a,b,c,d]

Page 11: Programming Languages and Paradigms 6.1 Characteristics of ...blk/cs3490/ch06/ch06.01.02.pdf · Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris 6.1 Characteristics

Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris

Enter this query in the Prolog interpreter and generate all possible solutions by typing a ; after the previous solution. Eventually Prolog will respond with “no” meaning there are no more possible solutions. Our next rule for lists will involve finding the length of a list. First we must explore how Prolog evaluates arithmetic expressions. Consider the increment rule: increment(N,Nplus1):- Nplus1 is N + 1.

The first argument is the input number and the second argument is the returned incremented value. The “is” has the format of the target variable on the left side and the arithmetic expression on the right side. The arithmetic expression is evaluated and is bound to the target variable. It should be noted that using “is” does not allow you to give the result and expect the input. So the query

increment(N, 6) would fail.

Exercise: Since increment is not reversible, write a decrement rule.

We combine the ideas in our last rule and our increment rule to write a rule

that finds the length of a list; we will call the rule len. The base case is the

empty list that has length 0. len([ ], 0).

In the general case we remove the first item from the list, find the length of the tail, and then add one to that result. len([ _ | Tail], NPlus1):- len(Tail, N), NPlus1 is N+1.

We will now challenge you to write a reasonably difficult rule; we want to reverse a list. Consider the query ?-rev([a,b,c,d],L).

L = [d,c,b,a]

We know we can decompose a list into the head and tail, so [a,b,c,d] splits into

the head a and the tail [b,c,d]. Let’s assume recursion works, so calling rev on

the tail produces [d,c,b]. We can use the append rule discussed previously to

put this result together with the head value, a.

Exercise: Complete the rev rule described above and test it. Don’t forget to

include the rule for the base case. Suppose we want to delete the first occurrence of a specified item from a list. We

will name the rule delFirst: ?-delFirst(b, [a,b,c,b,a,b,c], Result).

Result = [a,c,b,a,b,c]

We need two base cases: the list became length or the head of the list matches the item we are looking for in which case it is removed. Exercise: Write these two base cases.

Page 12: Programming Languages and Paradigms 6.1 Characteristics of ...blk/cs3490/ch06/ch06.01.02.pdf · Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris 6.1 Characteristics

Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris

We now need to write the general case where head of the list does not match the item we are looking for must be preserved and returned in the final result. Exercise: Write this general case and test your rules with a wide variety of data, including limiting cases.

Exercise: Once you have the delFirst rules working correctly, write a set of del

rules that delete all occurrences of an item at the top level. For example, ?-del(b, [a,b,c,b,a,b,c], Result).

Result = [a,c,a,c]

The lists we have discussed so far have been “flat” in the sense that the items in the list are primitive values such as atoms or integers. Prolog also allows nested lists, such as: [ a, [b, c, [d, e], f, g], h, [i, j] ]

For a “flat list” the len rule would tell us the number of items in the list, but if we

applied the len rule to the list given above it would return 4, the number of items

in the list at the top level. Our goal is to write a rule, we will name it countAll,

that returns the total number of items in the list regardless of the nesting level.

For the list shown above countAll would return 10. With flat lists we processed

the head element in some way and then made a recursive call on the tail of the list. But now we have a situation where the head of a list may be a list itself. We need to recurse both on the head of the list and the tail of the list and then combine the two results. We also need multiple base cases. The first base case is one we have seen before, we reach an empty list. For our countAll rule the empty list contains no items so we set the second parameter to zero. countAll([ ], 0).

The other base case is when we reach a single item that is not in a list. We will assume that item is an atom or an integer; that single item will add 1 to the overall count. countAll(X, 1):- atom(X); integer(X).

We have shown two built-in rules, to test whether an item is an atom or an item is an integer. We have also introduced the semicolon (;) on the right hand side of a rule; this indicates an “or” of the goals on either side. To complete the countAll rule we need to have a rule for the general case. It is actually very easy; you need to recurse on the head of the list and the tail of the list then you combine the results, which means in this case you add the two counts together. countAll([Head | Tail], N) :-

countAll(Head, N1), countAll(Tail, N2), N is N1+N2.

Exercise: Enter the countAll rule into your lists.pl file and test it out with a

variety of limiting cases, such as [ [ [ [ ] ] ] ] which should return 0.

In the next exercise you will test your understanding of processing nested lists and using double recursion. We will call the function deleteAll; it will remove all occurrences of a specified item at all levels of nesting structure.

Page 13: Programming Languages and Paradigms 6.1 Characteristics of ...blk/cs3490/ch06/ch06.01.02.pdf · Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris 6.1 Characteristics

Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris

?-deleteAll(a,[a,[b,a,[b,a],a,b],b,[a,b]], L).

L = [[b,[b],b],b,[b]]

Exercise: Write the deleteAll rule and test it with a variety of data, including

limiting cases. What is returned if you delete a from [a,[a,[a,[a]]]] ?

Here is one more exercise to test your knowledge of processing nested lists in Prolog. The goal is to write a flatten rule that removes all internal list structures. ?-flatten([a,[b,a,[b,a],a,b],b,[a,b]], L).

L = [a,b,a,b,a,a,b,b,a,b]

Exercise: Write the flatten rule and test it with a variety of data, including

limiting cases. Make sure a list that is already flat is not changed. 6.2.3 Using Data Tuples In this section we will investigate the use of Prolog to represent and process data structures, such as a linked list or a binary search tree. Our single-linked list conceptually may be pictured as:

5 2 3 nil

We will use a node data object where the first item is an integer value and the second item is the remainder of the list, which might be nil. The list above would be represented in Prolog as: node(5, node(2, node(3, nil)))

To create this list we need an insert rule that will add a new item at the end of the list. Here is how it would work: ?-insertSL(4, node(5, node(2, node(3, nil))), SLList).

SLList = node(5, node(2, node(3, node(4,nil))))

We need one base case to deal with a list that is initially empty: insertSL(X, nil, node(X,nil)).

The other base case is when you go through the list and reach the last node; this is where you will insert the new item. insertSL(X, node(Y,nil), node(Y,node(X,nil))).

In the general case you want to go down the list, node-by-node, until you reach the last node. You must be careful to preserve the list structure that comes before the last node. Here is the general case rule: insertSL(X, node(Y, Rest), node(Y,SLList):-

insertSL(X,Rest,SLList).

It sometimes becomes laborious to continually type in test cases into the Prolog interpreter. It is generally a good practice to write some test rules, such as the rule below to test insert. testInsertSL(X):- X=insertSL(2,insertSL(3,

insertSL(4,insertSL(5,nil))));

Page 14: Programming Languages and Paradigms 6.1 Characteristics of ...blk/cs3490/ch06/ch06.01.02.pdf · Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris 6.1 Characteristics

Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris

Exercise: Start a new file called datastructures.pl and enter the rules for insert above and the test rule. Write some additional test rules, including limiting cases.

Exercise: Write a rule called containsSL that succeeds if the specified data

item is contained in the single-linked list and fails otherwise. For example: ?-testInsertSL(X), containsSL(3, X). % succeeds

?-testInsertSL(X), containsSL(1, X). % fails

Exercise: Modify the insertSL rule to become an insertInOrderSL rule that keeps the list of integer values in sorted order from the smallest integer value to the largest integer value. Duplicates should be allowed. We now investigate how Prolog could be used to represent and process a binary search tree (BST). Recall that a BST is a binary tree that obeys the following property:

a node is nil or

a node contains left and right subtrees where the left subtree only contains values less than the value at the specified node and the right subtree only contains values greater than the value at the specified node

We will assume unique integer values in the tree. Here is a picture of a three with five nodes:

5

nil 2 nil 7

nil 4 nil nil 10 nil

Our BSTnode will contain three fields, the left subtree, the integer value at the

node, and the right subtree. So the tree above would be represented as: BSTnode(BSTnode(nil, 2, BSTnode(nil, 4, nil)), 5,

BSTnode(nil, 7, BSTnode(nil, 10, nil))).

Our first rule for processing a BST will be called containsBST that will succeed

if a particular item is in the tree and fail otherwise. The base case is where we find the desired item: containsBST(X, BSTnode(_, X, _)).

Notice that we have not specified variable names for the left and right subtrees so that we avoid the singleton variable warning. There are two general recursive cases: one where the search continues in the left subtree and the other where the search continues in the right subtree. containsBST(X,BSTnode(Left,Y,_)):- X<Y, containsBST(X,

Left).

containsBST(X,BSTnode(_,Y,Right)):- X>Y, containsBST(X,

Right).

Page 15: Programming Languages and Paradigms 6.1 Characteristics of ...blk/cs3490/ch06/ch06.01.02.pdf · Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris 6.1 Characteristics

Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris

Our next task is to write a rule to insert a new item into a BST. Insertions are always at a leaf node. We need two base cases, one where the node is inserted as a left leaf and the other when a node is inserted as a right leaf. We will add in a third base case where the value to be inserted is already in the BST; in this case the tree should be unchanged. insertBST(X, BSTnode(nil,Y,Right),

BSTnode(BSTnode(nil,X,nil),Y,Right):- X < Y.

insertBST(X, BSTnode(Left,Y,nil),

BSTnode(Left,Y, BSTnode(nil,X,nil)):- X > Y.

insertBST(X, BSTnode(Left,X,Right), BSTnode(Left,X,Right)).

It is your task in the exercise below to complete the insertBST rules by writing the two general recursive cases where the insertion proceeds either in the left subtree or the right subtree, much as containsBST searches in either the left or right subtrees. You will also need a rule to handle the limiting case of inserting the first node into an empty BST.

Exercise: Complete the insertBST rule and enter it in your datastructures.pl

file. Test your rule by building several trees from scratch. Write these as

test1InsertBST, test2InsertBST, and so forth.

6.2.4 The Inference Engine When the Prolog inference engine is presented with a query, it tries to unify that query with facts and rules in the database until a solution is found with a particular binding of variables, if present, or no solution is found. This search for a solution proceeds in a deterministic manner, meaning that given the same set of facts/rules and the same query, Prolog will produce one or more solutions in the same manner each time. First we discuss the unification process. Two Prolog terms, call them t1 and t2, unify if there is some substitution of variable values that makes the terms identical. As a first example consider: f(X,a) and f(b,Y)

with the bindings of X = b and Y = a these terms unify. But consider f(X,a) and f(b,X)

this does not unify because the substitution for the first argument requires X to bind to b and the substitution for the second argument would require X to bind to a. Since X can only be bound to one single value, unification is not possible. g(X,2,Y) and g(3,Y,X)

does not unify. At first it may appear the binding of X to 3 and Y to 2 may work, but the third term requires that X binds to Y, in other words they have the same value. This is not the case so the unification fails. Sometimes there are multiple possible unifiers. Consider the top level of the family tree we studied in Section 6.2.1. The two terms parent(X,Y) and parent(rosa,Y)

would unify with {X = rosa and Y = douglas}. It would also unify with {X = rosa and Y = rita}. But the Prolog inference engine would not use either one of these

Page 16: Programming Languages and Paradigms 6.1 Characteristics of ...blk/cs3490/ch06/ch06.01.02.pdf · Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris 6.1 Characteristics

Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris

bindings because they are too restrictive. The Most General Unifier (MGU) is the smallest set of bindings that allows unification to happen. For this problem the MGU is {X = rosa}; notice that the variable Y is left unbound. Exercise: Find the Most General Unifier for each of the two terms given below or state that unification fails. Term1 Term2 Give MGU or say fail

foobar(1,A,A) foobar(A,A,1)

foobar(1,A,B,2) foobar(A,1,1,B)

foobar(A,1,B,2) foobar(1,A,1,C)

foobar(A,2,A) foobar(A,A,B)

foobar(A,3,B) foobar(B,A,2)

foobar(4,B,2,D) foobar(A,1,2,4)

When we have a single rule with terms appearing to the right of the :- these terms are processed in left to right order. If some term fails to unify then the system backtracks to the previous term and tries to find the next possible unification and then proceeds ahead in hopes of success. If there are multiple rules of the same form, they are attempted from top to bottom. This is expressed in the following algorithm: Set the current goal to the query entered by the user

While the current goal is not empty

Select the leftmost subgoal

If a rule matches this subgoal

Select the first rule listed

Form a new subgoal

Else backtrack

End if

End while

Succeed, there are no more goals to satisfy

To illustrate this process we try to solve an abstract problem that contains a single rule and three facts. Let’s first see if you can solve this problem.

Exercise: Given the following rules (numbers for reference purposes): 1. s(g(A)) :- m(A), n(A).

2. n(p(a)).

3. m(n(B)).

4. m(p(B)).

The query s(D) succeeds and binds D to what value? We trace the solution of this query step-by-step using the following format: solve([ list of goals ], bindings )

When the list of goals becomes empty, the bindings represent the solution. solve([s(D)], s(D) )

1. solve([m(A), n(A)], s(g(A)))

1. nothing

Page 17: Programming Languages and Paradigms 6.1 Characteristics of ...blk/cs3490/ch06/ch06.01.02.pdf · Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris 6.1 Characteristics

Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris

2. nothing

3. solve([n(n(B))], s(g(n(B))) )

1. nothing

2. nothing

3. nothing

4. nothing

4. solve([n(p(B))], s(g(p(B))) )

1. nothing

2. solve([], s(g(p(a))) ) % first solution D = g(p(a))

3. nothing

4. nothing

2. nothing

3. nothing

4. nothing

We can show this trace as a proof tree; to keep it simple we have removed the “nothing” alternatives.

solve[s(D)]

solve[m(A),n(A)]

solve[n(n(A))] solve[n(p(B))]

solve[]

In the next sequence of examples we will investigate the effect of goal order and the effect of rule order. Recall the append rule we wrote earlier: app([ ], L2, L2).

app([Head| Tail], L2, [Head | Result]):- app(Tail, L2,

Result).

We want to use the app rule to formulate two more rules: test if a list is a prefix of another list and test if a list is a suffix of another list. Both of these rules can be written in one line: prefix(X,Z):- app(X,Y,Z).

suffix(Y,Z):- app(X,Y,Z).

Our goal now is to write a rule, call it sublist, to test whether one list is a sublist of a second list. For example: ?- sublist([b,c,d], [a,b,c,d,e,f]).

yes

?- sublist([e,f,g], [a,b,c,d,e,f]).

no

Page 18: Programming Languages and Paradigms 6.1 Characteristics of ...blk/cs3490/ch06/ch06.01.02.pdf · Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris 6.1 Characteristics

Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris

To understand how to write this rule we need to consider the following diagram:

X Y

S

Z

Two conditions must be satisfied: S is a suffix of X and X is a prefix for Z. Here are two alternative formulations of this rule, we number them 1 and 2: sublist1(S, Z):- suffix(S,X), prefix(X,Z).

sublist2(S, Z):- prefix(X,Z), suffix(S,X).

Exercise: Add the rules for suffix, prefix, sublist 1 and sublist 2 to your lists.pl file. Test both versions of sublist and see if you encounter any problems. The only difference between the two version of sublist is the order the goals are satisfied. It turns out that one version leads to an infinite computation for some queries. Can you tell which version will fail? Consider a query that will fail, sublist([a], [b, c, d]). % we will try both versions

Sublist1 first calls suffix(X, [a]) and the first solution discovered is X = [a]. Now sublist1 calls prefix([a], [b,c,d]) which fails. The Prolog inference engine now backtracks to the goal suffix(X, [a]) and tries to find another solution; the next solution is X = [ _, a]. The call to prefix([ _, a], [b,c,d]) fails and the system backtracks to suffix(X, [a]) which produces the solution X = [ _, _, a]. You should see what will happen: X is bound to a sequence of values, [a], [ _, a], [ _, _, a], [ _, _, _, a], [ _, _, _, _, a] … none of which will pass the prefix goal. This infinite computation causes the Prolog interpreter to fail. Now consider using sublist2 that calls prefix(X, [b, c, d]) binding X = [b]. The call to suffix([a], [b]) fails. The next call to prefix sets X = [b,c] which fails the suffix goal. The next call to prefix sets X = [b,c,d] which also fails the suffix goal. There are no more solutions for prefix(X, [b, c, d]) so the query fails, as expected. This example illustrates two points: (1) goal order can make a difference whether a rule is correct or incorrect (2) it is important to test your rules carefully; be sure to include queries that should fail as well as those that will succeed We now consider an example where rule order makes a difference. We write a second version of our append rule, we call it app2, where the base case is the second rule listed. app2([Head| Tail], L2, [Head | Result]):- app(Tail, L2,

Result).

app2([ ], L2, L2).

Page 19: Programming Languages and Paradigms 6.1 Characteristics of ...blk/cs3490/ch06/ch06.01.02.pdf · Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris 6.1 Characteristics

Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris

As we saw in the sublist1 example it is possible to cause Prolog to go into an infinite computation. That is true for app2; your task is to find a query that will cause this to happen. Exercise: Find a query for app2 that will cause an infinite computation. To find such a query you need to ask is there a data value that will cause the first rule for app2 to keep generating possible solutions. That won’t happen if the query binds to first two values to variables. Also if the third value is bound to a variable such as [a,b,c,d,e] but the first two values are not, they Prolog will generate a finite set of possible answers from [ ] [a,b,c,d,e] through [a,b,c,d,e] [ ]. So we need to think about leaving the one of the first two values unbound and the third value unbound. Since L2 is not changed by either rule, it is possible to bind it and let Prolog keep generating possible values for the first and third arguments, each becoming larger and larger. Consider the query: app2(L1, [c], R).

R is simply whatever L1 is with [c] appended on the end. One possible set of values is L1 = [ _ ] and R = [ _, c] where the value of the single item in L1 is not determined.

// off track need the Prolog interpreter to explain this better In philosophy it is generally agreed that just because you cannot prove something to be true given the current set of knowledge you cannot automatically assume it must be false. The same problem arises in Prolog: can we assume something that cannot be proven must then be false. Making such as assumption is called “negation as failure.” We first define a rule for “not. not(G) :- G, !, fail.

not(_).

The first rule is passed in the term G; if G succeeds, then not(G) should fail. So we test the goal G; assume it succeeds then the goal fail is called. As indicated by the name, this causes the not(G) to fail. Notice the ! between the call to G and fail; this is known as a cut. The cut prevents backtracking from right to left. It is appropriate in this case because we have already proved G to be true and don’t want to call it again (the second time it may be false even though it was true initially). So the cut prevents backtracking and retesting G. If G does not succeed the first time, then the second rule of the not is called and will always

Page 20: Programming Languages and Paradigms 6.1 Characteristics of ...blk/cs3490/ch06/ch06.01.02.pdf · Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris 6.1 Characteristics

Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris

succeed. If you define not then it must be used carefully. We now use it in our sibling rule: sibling(X,Y):-not(X=Y),mother(X,M),mother(Y,M),

father(X,F),father(Y,F).

At first it may appear that this rule works as before: ?- sibling(rosa,gregory).

yes

?- sibling(janet, jason).

no

But now consider the case where we ask Prolog to find any siblings in the database. ?- sibling(X,Y).

no

This is unexpected since we know there are several siblings, including rosa and gregory. Here is where adopting a strategy of negation as failure and placing the not(X=Y) before X and Y are bound gets us into trouble. Here is a revised version of the rule: sibling(X,Y):- mother(X,M),mother(Y,M),

father(X,F),father(Y,F),not(X=Y).

Now our request for Prolog to find siblings works as expected: ?- sibling(X,Y).

X = rosa

Y = gregory % other answers can be requested

6.2.5 Problem Solving Using Prolog Prolog is often used in the “real world” for natural language processing and in artificial intelligence. We will investigate parsing in the case studies in this and the next chapter. In this section we demonstrate how Prolog can be used in a search to solve a classical logical problem called the Farmer, Wolf, Goat and Cabbage puzzle. We first state the puzzle and see if you, as a human, can find a solution. The Farmer, Wolf, Goat and Cabbage are on the west side of the river and need to get safely to the east side. There is a boat big enough only to take a maximum of two at a time and one of those two must be the Farmer (the only one who can row the boat). It is possible for the Farmer to travel alone. If the Farmer leaves the Wolf and Goat alone on one side of the river, then the Wolf will eat the Goat. Similarly, if the Goat and Cabbage are left alone then the Goat will eat the Cabbage. The puzzle can be solved by devising a strategy that will get all four participants safely to the other side of the river. Exercise: Find a sequence of boat trips that will solve the Farmer, Wolf, Goat and Cabbage puzzle.

Page 21: Programming Languages and Paradigms 6.1 Characteristics of ...blk/cs3490/ch06/ch06.01.02.pdf · Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris 6.1 Characteristics

Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris

You first need to decide on a data structure to represent the current state of the puzzle. The sides of the river will be denoted by w and e for west and east respectively. You will store the current side of the river for all four entities in a tuple described logically as: (<Farmer side>, <Wolf side>, <Goat side>, <Cabbage side>)

So the initial state is (w,w,w,w) and the desired goal state is (e,e,e,e). After each boat trip the occupants of the boat will move to the opposite side of the river. Here are the rules for opposite. opp(e,w).

opp(w,e).

Many solutions to this puzzle can be found online or in textbooks; we have borrowed a solution presented in an Artificial Intelligence textbook by Luger and Stubblefield. Exercise: Explain the meaning of state(e,e,w,w). Is this a desirable state to have the puzzle in? A move is when the boat crosses the river; there are four possible moves: farmer takes self, farmer takes wolf, former takes goat, and farmer takes cabbage. You will print out a message every time a move is attempted. A move changes the state of the puzzle to a new state. Here is the move for the farmer taking the wolf. % move(State, NextState)

move(state(X,X,G,C), state(Y,Y,G,C)) :-

opp(X,Y),

not(unsafe(state(Y,Y,G,C))), % check if the move is safe

writelist(['try farmer takes wolf',Y,Y,G,C]).

You will study the “unsafe” rule shortly; first complete the other rules in the exercise below. Exercise: Start entering the rules for this puzzle into a file called FWGC.pl. Enter the opposite rules and the first move rule. Now enter the three other possible move rules into your file. % man takes the goat across the river

% man takes the cabbage across the river

% man takes nothing (but himself) across the river

In trying to solve a problem using search you often come to the point when no other move is possible to proceed forward. In this case the program must backtrack to a previous choice and select another alternative. Although it does not change the state of the computation, a fifth move rule is added to report when backtracking occurs. move(state(F,W,G,C), state(F,W,G,C)) :-

writelist([' BACKTRACK from:',F,W,G,C]),fail.

Notice the use of fail at the end of this rule.

Page 22: Programming Languages and Paradigms 6.1 Characteristics of ...blk/cs3490/ch06/ch06.01.02.pdf · Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris 6.1 Characteristics

Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris

We need to be able to determine if a state is safe (nothing gets eaten) or unsafe. If the wolf and the goat are on one side and the farmer on the opposite side, then the state is unsafe. Similarly if the goat and the cabbage are on one side and the farmer on the opposite side, then the state is unsafe. This is expressed in the following two rules: unsafe(state(X,Y,Y,C)) :- opp(X,Y).

unsafe(state(X,W,Y,Y)) :- opp(X,Y).

The program needs to remember the steps taken to reach a solution; this will be done using a stack. You will write the rules for this stack as an exercise. Here is the top level function named “go” that initiates the search for a solution. go(Start,Goal) :-

empty_stack(Empty_been_stack),

stack(Start,Empty_been_stack,Been_stack),

path(Start,Goal,Been_stack).

The query go(state(w,w,w,w),state(e,e,e,e)) will initiate the search.

The final two rules named “path” generate the search of the solution space. The first rule is used to detect when a solution is found. Since the moves have been

stored as a stack of states, the reverse_print_stack method prints the

moves from the first move to the final move. path(Goal,Goal,Been_stack) :- write('Solution Path Is:' ),nl, reverse_print_stack(Been_stack).

The solution to this puzzle is susceptible to cycles where the program can get hung up and not reach a solution. Here is one simple cycle of states: (w,w,w,w) (e,w,e,w) (w,w,w,w) (e,w,e,w) (w,w,w,w) (e,w,e,w) …. This can be prevented by making sure the program has not visited a state previously. Here is the recursive path rule: path(State,Goal,Been_stack) :-

move(State,Next_state),

not(member_stack(Next_state,Been_stack)),

stack(Next_state,Been_stack,New_been_stack),

path(Next_state,Goal,New_been_stack),!.

The ! at the end of this rule is the cut rule. This prevents the system from backtracking to a rule to the left of the cut. Your task in the exercises below is to complete the implementation of this program. Exercise: Some Prolog interpreters have a built-in not rule and some do not. Log

into your interpreter and type a query such as not(2 < 3). and see how the

interpreter responds. If it says it cannot find the not rule then you must complete this exercise, otherwise you can skip this exercise. Here is some guidance for implementing a not rule: • Test the goal that is passed in and if it succeeds then cause the not rule to

fail; put a cut (!) between the testing of the goal and the fail rule. • Otherwise you succeed

Page 23: Programming Languages and Paradigms 6.1 Characteristics of ...blk/cs3490/ch06/ch06.01.02.pdf · Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris 6.1 Characteristics

Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris

Exercise: Enter the code given previously (including the exercises) into the FWGC.pl file. Then complete the following four rules: stack, empty_stack, member_stack, and reverse_print_stack. Run the entire program by making the

query go(state(w,w,w,w),state(e,e,e,e)).

Investigate the program to answer the following questions:

Is there more than one solution? If so, how many?

Can the same program solve the puzzle starting on the east bank and ending up on the west bank?