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,

1. 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;
2. 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