15-212-X : Homework Assignment 4
Due Wed Oct 23, 10:00 am (electronically); papers at recitation.
Maximum Points: 100
Guidelines
- While we acknowledge that beauty is in the eye of the beholder, you
should nonetheless strive for elegance in your code. Not every program
which runs deserves full credit. Make sure to state invariants in comments
which are sometimes implicit in the informal presentation of an exercise.
If auxiliary functions are required, describe concisely what they implement.
Do not reinvent wheels and try to make your functions small and easy to
understand. Use tasteful layout and avoid longwinded and contorted code.
None of the problems requires more than a few lines of SML code.
- Make sure that your file compiles and runs. A program which doesn't
run will not get full credit and is likely to incur a heavy penalty.
- Homeworks must be all your own work.
- Late homeworks will be accepted only until start lecture on
Thursday, with a 25% penalty.
- If you have any questions about the assignment, contact Iliano Cervesato
at iliano@cs.cmu.edu or use cmu.andrew.academic.15-212-X.discuss.
Problem 1: Multisets (10 pts)
Multisets are collections that allow their elements to be repeated.
They differ from sets for permitting multiple occurrences of an element,
so that, for example, {a, a} is a different multiset from
{a}. They differ from lists since the order of the elements is
unimportant, so that, for example, {a, b} is the same multiset
as {b, a}.
You will be asked to implement a functor that realizes the following
signature for multisets, when instantiated with the proper arguments.
signature MSET =
sig
type item (* parameter *)
datatype mset = Empty | With of item * mset
exception MSet
val submset : mset * mset -> bool
val eq : mset * mset -> bool
val union : mset * mset -> mset
val diff : mset * mset -> mset
val toString : mset -> string
end;
This signature can be found in the file mset.sig.
The specifications for the declarations for signature MSET are as
follow:
- type item:
the type of the elements of the multiset. It is a parameter to the
signature and will be instantiated by means of a where
directive.
- datatype mset:
the type of multisets of elements of type item. Notice that
this type is concrete so that its constructors, Empty and
With are available for manipulation.
- exception MSet:
the exception to be raised if something goes wrong in any of the
operations below.
- val submset:
submset (m1, m2) returns true if every occurrence of an
element in m1 has a distinct corresponding occurrence in
m2. It returns false if either m1 contains
some element that does not appear in m2, or m1 contains
more occurrences of an element than m2.
- val eq:
eq (m1, m2) returns true if m1 contains the same
elements and in the same number as m2, not necessarily in the
same order.
- val union:
union (m1, m2) returns the multiset resulting by putting together
the elements of m1 and m2. The number of occurrences of
any element e of the union should be the sum of the number of
occurrences of e in m1 and m2.
- val diff:
diff (m1, m2) returns the multiset resulting by taking away
the elements of m2 from m1. If m2 is a
submultiset of m1, then the number of occurrences of any element
e of the difference should be the difference of the number of
occurrences of e in m1 and m2. If m2
is not a submultiset of m1, exception MSet is raised.
- val toString:
toString m converts m to a string. The representation of the
elements should be separated by commas (,) and the overall
multiset should be enclosed in braces ({...}). In particular the
empty multiset should be written "{}".
You are requested to write a functor MSet that, when supplied with
- a type item' for the elements of the multiset to be constructed,
- a function itemEq to test whether two such elements are equal,
and
- a function itemToString that converts an object of type
item' to a string,
returns a structure implementing multisets of with elements of type
item' according to the above signature.
The header of this functor is as follow (you can find it in mset.sig too):
functor MSet
(type item'
val itemEq : item' * item' -> bool
val itemToString : item' -> string)
:> MSET where type item = item' =
struct
(* ... *)
end;
Instantiate this functor to obtain multisets of integers and write an
expression that prints the result of evaluating (({1,2,1,3} union {2,3,4})
diff {2,1,4}).
Problem 2: Rewriting and Search (40 pts)
In class, we defined a simple rewriting system which relied on functions to
perform basic rewriting steps, offering a suitable set of operators to
combine basic steps into more complex behaviors. More precisely, we had the
following declarations for rewriting rules and combinators:
type 'a rewriter = 'a -> 'a
exception Fail
val THEN : 'a rewriter * 'a rewriter -> 'a rewriter
val ID : 'a rewriter
val ORELSE : 'a rewriter * 'a rewriter -> 'a rewriter
val FAIL : 'a rewriter
val TRY : 'a rewriter -> 'a rewriter
val REPEAT : 'a rewriter -> 'a rewriter
In this part of the assignment, we will extend this notion in a number of
directions:
- We will require that the application of rewrite rules returns a
validation that witnesses their use. Every basic rule will
be given a name and we will use lists of names as validations: the
validation of a rewriting sequence from an expression e to
an expression e' will be the list of the names of the rules
used to go from e to e'.
Building the validation as we apply rules would be a natural idea in our
setting, but unfortunately it does not scale up to more general search
problems. We will use a different strategy: validations will be
generated backwards from the final expression all the way back to the
expression we started with. Therefore, whenever a rule ris
applied to some expression e, it will return not only the next
expression e', but also a function that maps validations
v' from e' to the final expression
ef to validations v from e to
ef. In our case, v will simply be
nr::v', where nr is the name of
r.
- Our purpose will be to apply rules until some final state is reached.
Therefore we need some way to check whether a state resulting from the
application of some rule is final. A convenient way to achieve this
effect is to use continuations. As we saw in class, a continuation is a
function, that will be passed as input to our rewrite rules, and that
will tell us what to do next. We will use it to combine basic rules and
to check whether we have reached our final state.
On the basis of this description, the type declarations for the
REWRITE signature are as follows:
type object (* parameter *)
type validation = string list
type continuation = object * (validation -> validation)
-> object * (validation -> validation)
type rule = object * continuation -> object * (validation -> validation)
Notice that the expressions we want to rewrite, which have type
object, are a parameter to this signature: they will be instantiated
my means of a where directive. Observe also that continuations do
not map objects to objects, but operate on the entity returned by the
application of a rewrite rule: an object and a validation function.
The following combinators are defined in the signature REWRITE:
- exception Fail
This is the exception to be raised whenever a basic rewrite rule cannot
be applied.
- val ID : rule
ID rewrites an object obj and a continuation
k, by applying k to obj and the identity
validation.
- val FAIL : rule
FAIL is the rewrite rule that always fails, no matter what
object-continuation pair it is applied to.
- val THEN : rule * rule -> rule
r1 THEN r2 (feel free to declare it infix) is the rewrite rule
that results from first applying r1 and then r2.
Therefore, the application of r1 THEN r2 to an
object-continuation pair (obj,k) should call r1 on
obj and some continuation k' so that r2 gets
applied to the resulting state, say obj', and k. Pay
particular attention to the manner the validation functions returned by
r1 and r2 are combined. While implementing this
combinator, you might want to take advantage of the predefined infix
function val o : ('b -> 'c) * ('a -> 'b) -> ('a -> 'c), where
(f o g) is the function that applies g to its argument
and then applies f to the result.
- val ORELSE : rule * rule -> rule
r1 ORELSE r2 (again, feel free to declare it infix) is the rule
that behaves like r1 if this rule is applicable, and otherwise
behaves as r2.
- val REPEAT : rule * int -> rule
REPEAT (r, n) attempts to apply r exactly n times. If
n = 0, it behaves as the identity.
- val HOLDS : (object -> bool) -> rule
HOLDS p is the rule that when applied to an object-continuation
pair (obj,k) behaves as the identity if (p obj) is
true, and as FAIL otherwise.
- val UNTIL : rule * (object -> bool) -> rule
r UNTIL p (once more, feel free to declare it infix) is the rule
that repeats r until a state is reached where p holds.
We will be interested in verifying whether a final object obj2 is
reachable from an initial object obj1 by appropriately applying
basic rules from a set R. We take advantage of the above
combinators by expressing the alternative rules in R as a unique
disjunctive rule r by combining the individual rules in R
by means of ORELSE operators. If R offers a way of going
from obj1 to obj2, we want a validation to be returned in
order to know how the rules in R were chained.
A search function implementing the above specifications will therefore have
type
object * rule * object -> validation option
Such a function, let us call it search for the moment,
behaves as follows: search (obj1, r, obj2) attempts to
apply r to obj1 until a state that matches obj2 is
found. As we said, r will in general be an ORELSE
combination of basic rules.
The resulting value of this function can be:
- NONE if every attempt at finding an object that matches
obj2 ends up on a path where r is non-applicable.
In this case, we know that there is no way of going from obj1 to
obj2 by means of r.
- SOME(v) if a path from obj1 to obj2 is found.
In this case, the validation v is the list of the names of basic
rules in r that were chained on this path.
- non termination!
The simplest search strategy is depth first. Indeed the signature
REWRITE contains the declaration
val depthFirst : object * rule * object -> validation option
for it. depthFirst (obj1, r, obj2) will apply the first viable
alternative in r to obj1, then the first viable alternative
to the resulting object and so on until a state that matches obj2 is
eventually found. (Remember that r is in general a disjunction of
rules put together by means of the ORELSE combinator.) This is
however dangerous: depthFirst (obj1, r, obj2) could go in this way
down an infinite path (i.e. diverge) while a solution (a state matching
obj2) could have been found by using another alternative right at
the beginning.
depthFirst is almost immediate to implement on the basis of the
combinators above (do not be scared by the analysis in the previous
paragraph: it is really simple!). It is not satisfactory since a possible
solution might be missed because we made a wrong choice. Fortunately, there
are other search strategies that do not suffer from this problem, although
they are less efficient (and harder to implement!). We will consider here
iterative deepening. This strategy works by first checking whether
the initial state obj1 already matches the target obj2. If
this is not the case, it attempts to apply r exactly once but in all
possible ways to obj1. If no state matching obj2 is found
in this way, it tries to apply it exactly twice, again in all possible way.
And so on, it checks completely every level of the (implicit) tree
generated by applying the basic constituents of r to obj1
before moving to the next. In particular, if there is a way to match
obj2, then it will find it: this solution will require n
applications of r, but no attempt will be made to chain r
n+1 times before it has been ascertained that no solution can be
found in n moves.
The signature REWRITE contains the declaration
val iterativeDeepening : object * rule * object -> validation option
that will be used to implement iterative deepening.
All the declarations above have been collected in the signature
REWRITE, that you can find in the file rewrite.sig. Your task will be to implement a functor
Rewrite that, when given
- a type object' for the objects to be rewritten, and
- a boolean-valued binary function success, where success (obj,
obj2) will be used to determine whether the object obj
matches the specification obj2 for the final state of the
search (although equality is an obvious choice for this function, and it
is used in numerous case, it is often convenient to use some other
success function, as we will see in the next question),
returns a structure implementing a rewrite system with objects of type
object' according to the signature REWRITE.
In order to do so,
- study carefully the implementation of the combinators given in class
(remember that all the code is available on-line from the course Web page
- http://foxnet.cs.cmu.edu/15-212-X/home.html;
- take inspiration to Question 1 to define Rewrite: the technique
is very similar.
Problem 3: Multiset Rewriting (10 pts)
We will now combine the work done in Problems 1 and 2 and define a rewriting
system that operates on multisets. The following signature,
MSETREWRITE, contained in the file msetrewrite.sig, specifies the functionalities to
be provided:
signature MSETREWRITE =
sig
include REWRITE
val makeRule : object * object * string -> rule
end;
MSETREWRITE is identical to REWRITE (see Paulson pages
307-308 for a description of the include ML directive)
except for the addition of the function makeRule. In particular, the
parametric type object in this signature will be the type of
multisets of some unspecified type of items.
makeRule (m1, m2, name) creates a basic rewrite rule that rewrites a
state containing the multiset m1 to the state that differs from
it for the removal of all elements in m1 and their replacement with
the elements contained in m2. The validation function returned by
applying this rule simply appends name to the front of the
validation represented by its argument.
Your task will be to write a functor MSetRewrite that accepts as an
argument a structure M matching the signature MSET of
multisets and constructs a structure satisfying the above signature, in order
to implement rewriting on the objects constructed by means of MSET.
You should rely on the functor Rewrite implemented in the previous
question for achieving this task. In particular, there is no need to rewrite
adapted versions of the declarations contained in it: ML offers tools to
"inherit" these definitions.
Problem 4: Planning (40 pts)
We are going to use the work done so far to solve planning problems in the
blocks world. The block world consists of a table, a robot hand,
and a number of blocks. The robot hand can perform a set of basic actions on
the blocks: pick up a block from the table, unstack a block from another
block, stack two blocks and put down a block on the table. Clearly there are
constraints: the robot can hold at most one block at a time, and a block must
not have any block on top of it in order to be fetched by the robot. The
planing problem consists of finding a proper sequence of actions that
transform a given an initial configuration of the blocks to a target
configuration, and that always satisfies the constraints.
Here is an example:
||
*====*
| |
+---+
| C |
+---+ +---+
| B | | D |
+---+ +---+ ==> +---+
| A | | D | | A | ...
--------------------- ---------------------
|
The final situation on the right (notice that we do not care what happened to
blocks C and D, as long as they are not on D) can
be achieved by having the robot unstack C from B, put
it down, do the same with B, pick up D and stack it onto
A.
We will be interested in solving precisely the problem in this example. The
structure B, defined in the file block.sml,
contains already the declarations for it: you do not need to implement it.
Question 4.1: Situations (10 pts)
A situation is a description of the state of the world. It is a
multiset of the following components, or facts:
On(b1,b2) specifying that block b1 is on top of block
b2;
OnTable(b) specifying that block b is directly on the
table;
Clear(b) specifying that no block is on top of block b;
Holing(b) specifying that the robot hand is holding block
b;
ArmEmpty specifying that the robot hand is empty.
We will represent facts in our world by means of the
following type:
datatype fact = On of B.block * B.block
| OnTable of B.block
| Clear of B.block
| Holding of B.block
| ArmEmpty
This declaration, as all the code given below, can be found in the file blockworld.sml.
Your first task will be to define a structure Situations
implementing multisets of objects of type situation. Remember that in
Problem 1, you defined a functor that does precisely that. Notice also that
in order to apply it, you need to define functions to test for the equality
of facts (itemEq) and to generate a string corresponding to a given
fact (itemToString). Call these functions eqFact and
factToString, respectively.
Question 4.2: Basic Moves (10 pts)
We should now specify exactly the moves that are available in order to reach
the desired final block configuration from a given initial state. There are
four types of moves. We describe them by giving the facts that should be
part of the current situation to applied (preconditions) and the facts that
hold as a consequence of its application (postconditions). The new situation
is obtained by withdrawing the preconditions and adding the postconditions.
- stack(b1,b2) can be applied if the robot is holding b1
and b2 is clear from other blocks; it modifies the current
situation by having b1 on b2 with no block on top of
it, and no block in the arm of the robot.
Preconditions: Holding(b1), clear(b2).
Postconditions: On(b1,b2), EmptyArm, clear(b1).
- putdown(b) puts b on the table.
Preconditions: Holding(b).
Postconditions: OnTable(b), EmptyArm, clear(b).
- unstack(b1,b2) fetches b1 from the top of
b2.
Preconditions: On(b1,b2), EmptyArm,
clear(b1).
Postconditions: Holding(b1), clear(b2)
- pickup(b) picks b up from the table.
Preconditions: OnTable(b), EmptyArm,
clear(b).
Postconditions: Holding(b).
Our next task will be to implement these moves as basic rewrite rules over
situations (objects of Situations.object). However, we cannot do it
right away! Indeed, moves as described above are parametric on one or more of
blocks. Certainly, we could write one rule (of type
Situations.rule) for each instance of these moves on the basis of
the blocks declared in the structure B above. This is however
inefficient since only few of them are indeed applicable to a given situation.
We will instead proceed in a different way and generate on the fly the rules
relevant to the current situation. The idea is to write a single rule that,
when applied to some situation, analyzes it, generates all the moves that are
applicable to it, combines them by means of the ORELSE operator, and
applies the resulting rule to the same situation.
Since we want to use ORELSE and other combinators, creating a
structure for a multiset rewriting system over situations will be useful. Use
the functor MSetRewrite to create this structure, that you will call
SituationRewrite.
Let us now proceed with the above plan. In order to generate the rules to be
applied in a given situation, the following functions will turn out handy:
- val holding : Situations.mset -> B.block list.
holding s collects the list of every block b such that
the fact Holding(b) occurs in s.
- val clear : Situations.mset -> B.block list.
clear s makes a similar list with all blocks b such that
the fact Clear(b) occurs in s.
- val onTable : Situations.mset -> B.block list.
onTable s makes a list with all blocks b such that
the fact OnTable(b) occurs in s.
- val on : Situations.mset -> (B.block * B.block) list.
on s makes a list with all pairs of blocks (b1,b2) such
that the fact On(b1,b2) occurs in s.
- val armEmpty : Situations.mset -> bool.
armEmpty s returns true if the fact ArmEmpty
occurs in s, and false otherwise.
Implement them.
Question 4.3: Combining Basic Moves (10 pts)
These functions will allow us to construct lists of triples
(preconditions, postconditions, name) for each rule, as we will see
shortly. Given a triple of this form, the function
SituationRewrite.makeRule generates a rule implementing the desired
behavior. Since each of the move templates above can have several instances
in a given current situation, we need to combine them by means of the
ORELSE operator of structure SituationRewrite. Define a
function
val triplesToTactics : (Situations.mset * Situations.mset * string) list
-> SituationRewrite.rule
so that triplesToTactics triples calls
SituationRewrite.makeRule on each triple in triples and
combines them by means of SituationRewrite.ORELSE (return
SituationRewrite.FAIL if triples is empty).
We will now define four generic rules, stack, putdown,
unstack and pickup, that, when applied to a
situation-continuation pair (s,k), generate the ORELSE
combination of all rules of the appropriate move type that are applicable to
the situation s. As an example, we show the implementation of
unstack (for convenience, we opened the structures
Situations and SituationRewrite:
(* val unstack : SituationRewrite.rule *)
fun unstack (sk as (s,k)) =
let
fun thin' _ nil triples = triples
| thin' (bb as (b1,b2)) (b::clears) triples =
if B.eq (b1, b)
then
thin' bb clears
((With(On(bb),With(ArmEmpty,With(Clear(b1),Empty))),
With(Holding(b1),With(Clear(b2),Empty)),
"unstack(" ^ (B.toString b1) ^ ","
^ (B.toString b2) ^ ")")
::triples)
else thin' bb clears triples
fun thin nil _ = nil
| thin (bb::ons) clears =
thin' bb clears (thin ons clears)
in
if armEmpty s
then triplesToTactics (thin (on s) (clear s)) sk
else FAIL sk
end
Following a similar pattern, implement stack, putdown and
pickup.
The ORELSE combination of these for generic rules is a rule that
computes all the instances of moves applicable to the current state.
Question 4.4: Planning (10 pts)
We have reached the point to solve the planning problem in the example above.
Give expressions for the initial and target situations in that example and
compute a plan leading from the former to the latter. Which search strategy
will you use for this purpose? What goes wrong if you use the other one?
Hand-in instructions
- Put your SML code in the handin directory
is
/afs/andrew/scs/cs/15-212-X/studentdir/<your andrew
id>/ass4,
one file for each of the four problems. This file should contain the
signature and the structure, functor or other code you are requested to
implement. For Problem 4, copy also the file block.sml.