15-212-X : Homework Assignment 4

Due Wed Oct 23, 10:00 am (electronically); papers at recitation.

Maximum Points: 100


Guidelines


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.

This signature can be found in the file mset.sig.

The specifications for the declarations for signature MSET are as follow:

You are requested to write a functor MSet that, when supplied with 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): 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: In this part of the assignment, we will extend this notion in a number of directions: On the basis of this description, the type declarations for the REWRITE signature are as follows: 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:

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

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:

The simplest search strategy is depth first. Indeed the signature REWRITE contains the declaration 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

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

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: 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: We will represent facts in our world by means of the following type: 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. 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:

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 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:

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