| |
Computer Science 60
Principles of Computer Science
Fall 2009
Assignment 8: Prolog - yes! [100 points]
Due Monday, November 2 by 11:59 PM
Submitting... For this assignment, you'll submit the
two files: lists.pl and simpsons.pl. Starter versions
of these files are linked from the main assignment page.
Formatting and commenting...
Please use the standard commenting
convention of your name, file name, and date at the top of each file.
THIS WEEK WE WILL TAKE OFF POINTS IF THIS COMMENT IS MISSING! In addition, be sure to include a comment for each function
that you implement. Examples appear through the simpsons.pl starter file.
Finally, please be sure to name your files and functions
as noted by the problems.... The grading procedure for this assignment is
partially automated and files and functions whose spellings differ from
those below will cause our scripts to "barf."
Part 0: Getting started... [0 points]
First, you will want to be sure you can run prolog -- either from
your computer or from knuth.
Instructions from knuth:
- Log in to knuth. Create a new folder (probably) for your hw #8 files. Copy the
starter files, named simpsons.pl and lists.pl to your directory using WinSCP or some equivalent program.
-
You can start prolog and try out its interface by simply typing
pl
at the command prompt.
- You can load a file with
pl -f simpsons.pl
or any other file name you'd like.
- You might try predicates such as
parent(X, bart).
once the simpsons.pl file is loaded. Recall that
the semicolon means OR, and asks for more
possible solutions. Hitting return stops the search for possible
solutions to a predicate query.
-
You will notice that the simpsons.pl file, as written, will
cause Prolog to warn you about "singleton" variables.
These warnings will (or, at least should) disappear when
you write the predicates that
are described for this week's hw, below.
Instructions from your own machine:
- You can obtain a version named SWI prolog
from for either Windows or
Mac OS X. The Mac version requires you to have X11 (for the graphical interface) installed (if you have a
recent enough Mac, you probably do). The location of the program after it installs on a Mac is /opt/local/bin/swipl.
On the Mac, you'll need to run it from the command line (similar to the instructions above). On a PC,
you'll have a window that opens for the Prolog "command line" and others for editing your Prolog source files.
- Warning: the SWI prolog versions noted above use true and false instead of yes
and no in repsonding to queries. This decreases Prolog's joke-making ability by several orders of magnitude!
One important note before you get started...
Unlike programming languages we've considered so far: you
must define everything in a file. The prolog command line can only be
used to make queries based on the facts and rules in the file.
Therefore, the following will NOT work:
?- [simpsons]. <-- Loads in the file simpsons.pl
% simpsons compiled 0.42 sec, 4242 bytes
Yes
?- parent(bart, minibart).
This is attempting to define a new fact inside the prolog environment
and prolog will "barf".
Part 1: The Simpsons' family tree...
[40 points (10 points each), individual]
This part asks you to write predicates that define family relationships
and answer questions about the Simpsons' family tree -- or, at least, the
version of the family tree that appears in simpsons.pl.
You should download that simpsons.pl file -- you may need to change its name
to simpsons.pl from simpsons.txt, depending on how you
get the file. If it's already simpsons.pl, it's OK. (This is because some browsers
believe that files ending in .pl are Perl scripts.)
In that file, there are rules for a (fictional) Simpsons' family
tree. These rules define, from scratch, the predicates
- person
- parent
- female
- male
- age
In addition, there are example prolog predicates that
define child, mother, and ancestor relationships,
based on the above primitive rules.
Finally, at the very top of the file, there are four placeholders where you should
replace the fail. with rules that define each of the predicates described
in detail below. The predicates to write are
- grandparent
- cousins
- hasYS
- hasFirstGC
You may also add any additional
helper predicates that you wish,
including anything that we looked at in class. However, please don't
change the facts about parenthood/relationships among the Simpsons!
The Simpsons' Family-tree predicates to write:
- grandparent(X, Y) should be true if and
only if X is a grandparent of Y.
You can test this out with
grandparent( john, bart ). % yes! grandparent( lisa, bart ). % no. grandparent( X, bart ). % there will be four answers
In fact, you can check all four of the answers to the last query above
using the setof predicate. Try this -- it will be useful
throughout using Prolog!
setof( P, grandparent( P, bart ), Answer ). Answer = [homericus, jackie, john, matilda]
Thus, setof is a three-input predicate whose first input is a variable
and whose second input is a predicate using that variable. Then, setof
tries to find all of the values for that variable that satisfy the predicate.
Those values are collected into a list and bound to the name that is the third
and final input to setof. Crazy (but helpful)!
- cousins(X, Y) should be true if and only if
X and Y are first
cousins. Note that for the sake of this problem (and perhaps in general),
a person cannot be their own cousin. Similarly, do not
consider brothers and sisters to be cousins for this problem.
You can test this out with
cousins( bart, lisa ). % no cousins( bart, terpsichore ). % yes
setof( C, cousins(C,lisa), Answer ). % lisa has 2 cousins here Answer = [millhouse, terpsichore]
- hasYS(X) should be true if and only if X
has a younger sister. (Consider "younger" to mean strictly less-than in
age.)
You should check this with some queries of your own devising;
in addition, you can check for all of the people with younger sisters with
setof( P, hasYS(P), Answer ). % everyone with a younger sister
Answer = [atropos, bart, klotho, lisa, patty, selma]
- hasFirstGC(X) should be true if and only if X
has both a parent P and a child C, such that C is P's
oldest (hence "first") grandchild. The predicate hasFirstGC(X) should be
false if X has no parent or has no child (or does not have a child that
matches the criterion above).
Hint: notoldest that we looked at in class won't help
outright on this problem, but consider writing a variant, perhaps named
notoldestgrandchild( GP, GC ), which is true if GC is
not GP's oldest grandchild. Then the negation
example we looked at in class will serve as a good template!
You should check this with some of your own queries;
in addition, you can check for all of the people with first grandchildren via
setof( P, hasFirstGC(P), Answer ).
Answer = [esmerelda, homer, marge, matilda, skug]
Prolog reminders
Recall that "," is the symbol for AND, ";" is the SYMBOL for OR, and
"\+" is the symbol for NOT, while \== is the symbol for "not equal."
Please look at the class notes for other
(admittedly, sometimes crazy) Prolog syntax. Another good resource is Learn Prolog Now!,
a site that explains the language concisely and with a number of examples.
Please submit your solutions in your simpsons.pl file under assignment #8.
Note on Prolog's output formatting
Prof. Keller has found some of the settings that control how Prolog
formats its output: the following describes a few of those settings
you may want to change.
You may have noticed that when answer variables get bound to long lists,
swipl shows the list with '...' after about 10 elements,
which can be annoying.
To see the entire list, simply type
w
following the output. For example, if the following rules are
defined:
range(L,H,[]) :- L > H. range(L,H,[L|R]) :- M is L+1, range(M,H,R).
and then you try the query
?- range(1, 50, X).
you will see the result
X = [1, 2, 3, 4, 5, 6, 7, 8, 9|...]
By typing a w when it pauses, Prolog will write the
whole list out (all 50 elements):
X = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50]
To change the limit on the number of elements once and for all, copy the
following into your source file, or copy
everything but the first two characters at the Prolog prompt:
:- set_prolog_flag(toplevel_print_options, [quoted(true), portray(true), attributes(portray), max_depth(999), priority(699)]).
You may use any other value for max_depth, including 0, which will
place no limit on the depth of the expression. The default value of
max_depth is set at 10. The values of the other attributes are just
the default ones.
Part 2: Lists in Prolog
[60 points, pair or individual]
Here, you will write four Prolog predicates that express
relationships about lists (and trees and graphs, expressed as lists). Please submit these
rules in one file called lists.pl. This link has a
starting point with placeholders. As before, you'll need to change the extension from
.txt to .pl.
You may create and use any helper rules that you like. In particular,
some of the rules that we saw in class may be helpful here! For example,
we wrote the member predicate:
member( X, [X|_] ). member( X, [_|R] ) :- member( X, R ).
This is built in to some versions of prolog, but not others, so you may
want to include it!
The Prolog list predicates to write:
In this section you will implement 4 predicates: 3 of which
are specified, and the last is your choice of 3 possibilties. In
other words you will implement:
- ALL of: removeOne, count, and find
- ONE of (your choice): depth, insert, or path
Details on the Prolog list predicates to write:
- removeOne(E, List, NewList) should be true if and only if E is an element of
list List one or more times, and the list NewList should be identical to
List except that
the element E is removed once.
Here are three example queries and their resulting Prolog interactions:
removeOne( a, [a,b,c,a], R ). R = [b, c, a] ; R = [a, b, c] ; No
removeOne( d, [a,b,c,a], R ). No
removeOne( E, [a,b,c,a], R ). E = a, R = [b, c, a] ; E = b, R = [a, c, a] ; E = c, R = [a, b, a] ; E = a, R = [a, b, c] ; No
For full credit, your removeOne predicate must be able to work when either the first
or last argument, or both, are variables, as demonstrated above. You should assume that
the middle argument is bound to a list value (that is, it's not a variable).
Keep in mind the requirement that E be a member of
List!
The removeOne predicate requires no more than two rules. If you're writing
more than three (or even two) ~ take a step back and reconsider! In particular, remember that
in Prolog, negation is expressed through failure of its search. Thus, you don't
want a base case involving the empty list as the second argument!
After all, the rules should simply fail when
passed an empty list (directly or through recursion); that is, there should be no such rule at all.
- count(X, L, N) should be true if and only
if item X occurs in list L
exactly N times. For full credit, your predicate should work
when X and/or N are variables (or both). It does not
need to work when L is a variable.
Here are two examples:
count(spam, [oh, spam, spam, we, love, spam], N). % user query N = 3 ; % prolog reply No % no more possibilities
and a case where both X and N are variable:
count(X, [oh, spam, spam, we, love, spam], N). % user query X = oh, N = 1 ; % any more? X = spam, N = 3 ; % any more? X = we, N = 1 ; % any more? X = love, N = 1 ; % any more? no % no more possibilities
It's OK to get one more result in the latter case, above, with
N = 0
and either no X at all or an unnamed variable X. Whether or
not you get this final possibility depends on where (if at all) your code
forces things to be non-variable (bound to a value)
with the nonvar or var predicates. Either is ok. Since we didnt' get a chance to talk about var and nonvar, don't worry about them, unless you feel like playing around with them on your own.
hen writing the count predicate, be careful that it "counts" exactly the
correct number of occurences. One pitfall is to write a version of
this function that returns all values of X less
than or equal to the correct count.
The count predicate requires no more than three rules. (You might re-design
things if your code is extending much beyond that... .)
- One-dimensional pattern matching is an important task in
many applications such as cryptography and DNA sequencing. For this problem we encode
a 1d pattern as a list of items (named Pattern) and we'll search for that
pattern within another list called the Target.
Thus, for this problem write the rule find(Pattern, Target,
Index) which takes two lists Pattern and Target
and a non-negative integer Index as input. This
find rule should be true if and only if Pattern occurs inside
list Target beginning at position Index.
The find predicate should not be true in the
case that Pattern is the empty list -- so your base case will
need to be bigger than the empty list!
For example, here is a Prolog query and response:
find([1, 2], [1, 2, 1, 2, 1], 0). % user query More? % this prompt is prolog-dependent Yes % the user hit return (not a semicolon)
The extra More? can be annoying when trying to test to see if
a fully-instantiated query succeeds or not. By printing that More?
Prolog is saying that it did succeed at the query -- it's
then asking if you'd like it to try to succeed again through a different
line of reasoning. You're probably not worried about that,
so you can simply hit return.
Nonessential aside on prolog internals
If you'd like an immediate Yes-or-No answer, you can cut off Prolog's future
searching with the cut operator, the exclamation point: !. Thus,
find([1, 2], [1, 2, 1, 2, 1], 0), !. % user query ~ one search only! Yes % prolog succeeded
Using ! is not required, but it's there if you'd like to.
Alternatively, you can tell Prolog to stop being ridiculous
via the line
set_prolog_flag( prompt_alternatives_on, groundness ).
and it will stop the extra-prompting (unless there are variables present).
End of nonessential aside
Here are two more examples:
setof( N, find( [1,2], [1,2,1,2,1], N ), Answer ). Answer = [0, 2]
and
find([], [1, 2, 1, 2, 1], N). No.
Remember that find should not succeed for the empty Pattern.
For full credit, your find predicate should work when either or
both of Pattern and Index are Variables. We will only
test your code when Target is bound to a value (it's a non-variiable).
find( Pattern, [a,c,a,c], N ). Pattern = [a], N = 0 ; Pattern = [a, c], N = 0 ; Pattern = [a, c, a], N = 0 ; Pattern = [a, c, a, c], N = 0 ; Pattern = [c], N = 1 ; Pattern = [c, a], N = 1 ; Pattern = [c, a, c], N = 1 ; Pattern = [a], N = 2 ; Pattern = [a, c], N = 2 ; Pattern = [c], N = 3 ; No
You can coax setof to give you all of these at once, if you'd like:
setof( Pair, P^N^(find( P, [a,c,a,c], N ), Pair=[N,P]), Answer ). Answer = [[0, [a]], [0, [a, c]], [0, [a, c, a]], [0, [a, c, a, c]], [1, [c]], [1, [c, a]], [1, [c, a, c]], [2, [a]], [2, [a, c]], [3, [c]]]
The caret ^ acts as a "there exists" operator, i.e., the set of
Pair values such that there exists a P and an
N that satisfies the expression
(find( P, [a,c,a,c], N ), Pair=[N,P])
Yes - crazy, but true.
The find predicate requires no more than three rules. (You might re-design
things if your code is extending much beyond that... .)
Finally,
implement ONE of the following 3 predicates about trees and graphs.
To repeat: YOU ONLY NEED TO IMPLEMENT 1 of: depth, insert OR
path. You may implement all three for up to +15 points extra
credit.
-
depth(E, Tree, N) should be true if and only if E is an element of
the binary search tree Tree at a depth of N. The root of the
tree is defined to be at depth 0. The children of the root are all at depth 1,
their children at depth 2, and so on.
For full credit, your depth predicate should work in the case that
either or both E and/or N are variables or bound values.
You should assume that Tree is, in fact, a binary search tree --
your code should not try to check this.
You'll be able to use the tree below by including in your code the tree1
predicate, as follows:
tree1( [ 42, [ 20, [10, [5, [], []], [18, [], []]], [] ], [ 100, [67, [50, [], []], []], [150, [], [2009, [], []]] ] ] ).
Then you can use this to bind a variable to the above data before testing.
The examples below illustrate this.
Here are some example queries and their resulting Prolog interactions:
tree1( T1 ), depth( 42, T1, N ). % user query
T1 = [42, [20, [10, [5, [], []], ... % prints the tree N = 0 ; % user asks for more with the semicolon
No % no more depths for 42
tree1( T1 ), setof( E, depth( E, T1, 3 ), Answer ). % user query
T1 = [42, [20, [10, [5, [], []], ... Answer = [5, 18, 50, 2009]
The depth predicate requires no more than three rules. (You might re-design
things if your code is extending much beyond that... .)
-
insert(E, Tree, NewTree) should be true if and only if NewTree is a
binary search tree identical to Tree, except that the element E is
now present in NewTree in an appropriate place. It is OK if E is
already in Tree -- in that case, NewTree and Tree will
be identical.
For this predicate, the only input that needs to be handled as a variable (or value)
is the last one. You should assume that both E and Tree are
already bound.
Again, use this tree1 predicate:
tree1( [ 42, [ 20, [10, [5, [], []], [18, [], []]], [] ], [ 100, [67, [50, [], []], []], [150, [], [2009, [], []]] ] ] ).
Then you can use this to bind a variable to the above data before testing.
Here are two example queries and their resulting Prolog interactions.
The spacing has been adjusted to make it clear what's happening, but Prolog
will simply print the full tree out. (See the note between the Simpsons and
list-based problems to tell Prolog to always print all of its data.)
tree1( T1 ), insert( 43, T1, NewTree ).
T1 = [42, [20, [10, [5, [], []], [18, [], []]], []], [100, [67, [50, [], []], []], [150, [], [2009, [], []]]]], NewTree = [42, [20, [10, [5, [], []], [18, [], []]], []], [100, [67, [50, [43, [], []], []], []], [150, [], [2009, [], []]]]]
tree1( T1 ), insert( 41, T1, NewTree ).
T1 = [42, [20, [10, [5, [], []], [18, [], []]], []], [100, [67, [50, [], []], []], [150, [], [2009, [], []]]]], NewTree = [42, [20, [10, [5, [], []], [18, [], []]], [41, [], []]], [100, [67, [50, [], []], []], [150, [], [2009, [], []]]]]
The insert predicate requires no more than four rules. (You might re-design
things if your code is extending much beyond that... .)
-
path(A, B, Graph, Path) should be true if A and
B are both nodes in the graph Graph and if Path
is a list of nodes, beginning with A and ending with B
that connects those two with edges from Graph. You should
assume that Graph is acyclic. Also, you should
assume that every node A is reachable from A itself using the
path [A].
For full credit, your path predicate should work when A, B,
and/or Path are variables. However, you may assume that Graph is
a fully-instantiated list of edges -- again, it will not contain any cycles.
This time, you might use this graph1 predicate:
graph1( [ [a,b], [b,c], [c,d], [b,w], [w,x], [x,z], [b,z] ] ).
Then you can use this to bind a variable to the above data before testing.
Here are two example queries and their resulting Prolog interactions.
% small example ~ just finding P graph1(G1), path( a, z, G1, P ). % user query
G1 = [[a, b], [b, c], [c, d], [b, w], [w, x], [x, z], [b, z]], P = [a, b, w, x, z] ; % any more?
G1 = [[a, b], [b, c], [c, d], [b, w], [w, x], [x, z], [b, z]], P = [a, b, z] ; % any more?
No
% HUGE example ~ A, B, and P are all unbound graph1(G1), path( A, B, G1, P ). % user query
G = [[a, b], [b, c], [c, d], [b, w], [w, x], [x, z], [b, z]], A = B, P = [B] ;
G = [[a, b], [b, c], [c, d], [b, w], [w, x], [x, z], [b, z]], A = a, B = b, P = [a, b] ;
G = [[a, b], [b, c], [c, d], [b, w], [w, x], [x, z], [b, z]], A = a, B = c, P = [a, b, c] ;
G = [[a, b], [b, c], [c, d], [b, w], [w, x], [x, z], [b, z]], A = a, B = d, P = [a, b, c, d] ;
G = [[a, b], [b, c], [c, d], [b, w], [w, x], [x, z], [b, z]], A = a, B = w, P = [a, b, w] ;
G = [[a, b], [b, c], [c, d], [b, w], [w, x], [x, z], [b, z]], A = a, B = x, P = [a, b, w, x] ;
G = [[a, b], [b, c], [c, d], [b, w], [w, x], [x, z], [b, z]], A = a, B = z, P = [a, b, w, x, z] ;
G = [[a, b], [b, c], [c, d], [b, w], [w, x], [x, z], [b, z]], A = a, B = z, P = [a, b, z] ;
G = [[a, b], [b, c], [c, d], [b, w], [w, x], [x, z], [b, z]], A = b, B = c, P = [b, c] ;
G = [[a, b], [b, c], [c, d], [b, w], [w, x], [x, z], [b, z]], A = b, B = d, P = [b, c, d] ;
G = [[a, b], [b, c], [c, d], [b, w], [w, x], [x, z], [b, z]], A = c, B = d, P = [c, d] ;
G = [[a, b], [b, c], [c, d], [b, w], [w, x], [x, z], [b, z]], A = b, B = w, P = [b, w] ;
G = [[a, b], [b, c], [c, d], [b, w], [w, x], [x, z], [b, z]], A = b, B = x, P = [b, w, x] ;
G = [[a, b], [b, c], [c, d], [b, w], [w, x], [x, z], [b, z]], A = b, B = z, P = [b, w, x, z] ;
G = [[a, b], [b, c], [c, d], [b, w], [w, x], [x, z], [b, z]], A = w, B = x, P = [w, x] ;
G = [[a, b], [b, c], [c, d], [b, w], [w, x], [x, z], [b, z]], A = w, B = z, P = [w, x, z] ;
G = [[a, b], [b, c], [c, d], [b, w], [w, x], [x, z], [b, z]], A = x, B = z, P = [x, z] ;
G = [[a, b], [b, c], [c, d], [b, w], [w, x], [x, z], [b, z]], A = b, B = z, P = [b, z] ;
No
The path predicate requires no more than two rules! (Well, you need
the kid predicate we almost wrote in class:
kid( X, Graph, K ) :- member( [X,K], Graph ).
(You might re-design
things if your code is becoming much longer than that... .)
Submitting... Submit the two files: lists.pl and simpsons.pl.
|