CS 334: Lab 6: Tail Recursion and Visitors

Overview

In this lab you will practice two important concepts from the course. First, you will write tail-recursive functions in ML. Second, you will explore the Visitor Design Pattern in Java and compare it to a more traditional object-oriented design for extensibility.

Partner

You are encouraged to work with a partner on this lab.

Getting Started

Setting Up Your Repository

You will receive an email with an invitation link to the lab6 assignment on GitHub Classroom. You can follow the same instructions as on Lab 2 for accessing and cloning your repository. See the GitHub reference for instructions to add a partner. You should answer the following in the appropriate files in your repository.

Programming Problems

1. Tail Recursion (20 points)

  1. Dot Product. The dot product of two vectors [a_1,\ldots,a_n] and [b_1,\ldots,b_n] is the sum a_1 b_1 + a_2 b_2 + \cdots + a_n b_n. For example,

    [1,2,3] \cdot [-1,5,3] = 1 \cdot -1 + 2 \cdot 5 + 3 \cdot 3 = 18

    Implement the function

    dotprod: int list -> int list -> int
    

    to compute the dot product of two vectors represented as lists. You should write this using tail-recursion, so your dotprod function will probably just be a wrapper function that calls a second function that does all the work. If passed lists of different length, your function should raise a DotProd exception. You will need to declare this type of exception, but you need not catch it.

    - dotprod [1,2,3] [~1,5,3];
    val it = 18 : int
    - dotprod [~1,3,9] [0,0,11];
    val it = 99 : int
    - dotprod [] [];
    val it = 0 : int
    - dotprod [1,2,3] [4,5];
    uncaught exception DotProd
    

    Note

    After testing your code, you can comment out or remove the case that generates an exception so it doesn't cause problems when you go on to the next problem.

  2. Fib. The numbers in the Fibonacci sequence are defined as:

    \begin{array}{rcl} F(0) & = & 0\\ F(1) & = & 1\\ F(n) & = & F(n-1) + F(n-2) \end{array}

    Thus, the sequence is 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, etc.

    The following defines a function that returns the n-th Fibonacci number.

    fun slow_fib(0) = 0
        | slow_fib(1) = 1
        | slow_fib(n) = slow_fib(n-1) + slow_fib(n-2);
    

    Unfortunately, computing slow_fib(n) requires O(2^n) time.

    Define a tail recursive function fast_fib that can compute F(n) in O(n) time by using tail recursion. (As above, fast_fib will most likely be a wrapper that calls a tail-recursive function.) The tail-recursive function should have only one recursive call in its definition.

    - fast_fib 0
    val it = 0 : int
    - fast_fib 1
    val it = 1 : int
    - fast_fib 5
    val it = 5 : int
    - fast_fib 10
    val it = 55 : int
    

    Hint: When converting sumSquares to tail-recursive form, we introduced one auxiliary parameter to accumulate the result of the single recursive call in that function. How many auxiliary parameters do you think we will need for fibtail?

    Use the bash time function to see how much faster the tail recursive function can be, even for relatively small n. That is, call slow_fib(40) and time it, and do the same for fast_fib(40). Try 41, 42, ... (or smaller numbers if even 40 takes a long time.) You use time as follows:

    time sml < tail-recursion.sml
    

2. Visitor Design Pattern (20 points)

The code for this question is located in the ExprVisitor.java file. Include answers to the questions below as comments at the top of that file.

To compile and run the program, use:

javac ExprVisitor.java
java ExprVisitor
  1. The extension and maintenance of an object hierarchy can be greatly simplified (or greatly complicated) by design decisions made early in the life of the hierarchy. This question explores various design possibilities for an object hierarchy (like the one above) that represents arithmetic expressions.

    The designers of the hierarchy have already decided to structure it as shown below, with a base class Expr and derived classes Number, Sum, Times, and so on. They are now contemplating how to implement various operations on Expressions, such as printing the expression in parenthesized form or evaluating the expression. They are asking you, a freshly-minted language expert, to help.

    The obvious way of implementing such operations is by adding a method to each class for each operation. This version is not in the starter code, but the expression hierarchy would look like the following in this scenario:

    abstract class Expr {
        public abstract String toString();
        public abstract int eval();
    }
    
    class Number extends Expr {
        int n;
    
        public Number(int n) { this.n = n; }
        public String toString() { ... }
        public int eval() { ... }
    }
    
    class Sum extends Expr {
        Expr left, right;
    
        public Sum(Expr left, Expr right) {
            this.left = left;
            this.right = right;
        }
        public String toString() { ... }
        public int eval() { ... }
    }
    

    Suppose there are n subclasses of Expr altogether, each similar to Number and Sum shown here.

    How many classes would have to be added or changed to add a new class to represent division expressions.

  2. How many classes would have to be added or changed to add a new operation to graphically draw the expression parse tree.

  3. Another way of implementing expression classes and operations uses a pattern called the Visitor Design Pattern. In this pattern, each operation is represented by a Visitor class. Each Visitor class has a visitClass method for each expression class Class in the hierarchy. Each expression class Class is set up to call the visitClass method to perform the operation for that particular class. In particular, each class in the expression hierarchy has an accept method which accepts a Visitor as an argument and "allows the Visitor to visit the class and perform its operation." The expression class does not need to know what operation the visitor is performing.

    If you write a Visitor class ToString to construct a string representation of an expression tree, it would be used as follows:

    Expr expTree = ...some code that builds the expression tree...;
    ToString printer = new ToString();
    String stringRep = expTree.accept(printer);
    System.out.println(stringRep);
    

    The first line defines an expression, the second defines an instance of your ToString class, and the third passes your visitor object to the accept method of the expression object.

    The expression class hierarchy using the Visitor Design Pattern has the following form, with an accept method in each class and possibly other methods. Since different kinds of visitors return different types of values, the accept method is parameterized by the type that the visitor computes for each expression tree:

    abstract class Expr {
        abstract <T> T accept(Visitor<T> v);
    }
    
    class Number extends Expr {
        int n;
    
        public Number(int n) { this.n = n; }
    
        public <T> T accept(Visitor<T> v) {
            return v.visitNumber(this.n);
        }
    }
    
    class Sum extends Expr {
        Expr left, right;
    
        public Sum(Expr left, Expr right) {
            this.left = left;
            this.right = right;
        }
    
        public <T> T accept(Visitor<T> v) {
            T leftVal = left.accept(v);
            T rightVal = right.accept(v);
            return v.visitSum(leftVal, rightVal);
        }  
    }
    

    The associated Visitor abstract class, naming the methods that must be included in each visitor, and the ToString visitor, have this form:

    abstract class Visitor<T> {
        abstract T visitNumber(int n);
        abstract T visitSum(T left, T right);
    }
    
    class ToString extends Visitor<String> {
        public String visitNumber(int n) { 
            return "" + n;
        }
        public String visitSum(String left, String right) {
            return "(" + left + " + " + right + ")";
        }
    }
    

    Here is an example of using the visitor to evaluate and print an expression.

    class ExprVisitor { 
        public static void main(String s[]) { 
            Expr e = new Sum(new Number(3), new Number(2)); 
            ToString printer = new ToString();
            String stringRep = e.accept(printer); 
            System.out.print(stringRep); 
        }
    }
    

    Starting with the call to e.accept(printer), what is the sequence of method calls that will occur while building the string for the expression tree e?

  4. Add the following classes to the source file. You will need to change some of the existing classes to accomodate them:

    • An Eval visitor class that computes the value of an expression tree. The visit methods should return an Integer. Recall that Java has auto-boxing, so it can convert int values to Integer objects and vice-versa, as needed.

    • Subtract and Times classes to represent subtraction and product expressions.

    • A Compile visitor class that returns a sequence of stack-based instructions to evaluate the expression. You may use the following stack instructions (Refer back to HW 3 if you need a refresher on how these instructions operate):

      PUSH(n)
      ADD
      MULT
      SUB
      DIV
      SWAP
      

      The visit methods can simply return a String containing the sequence of instructions. For example, compiling 3*(1-2) should return the string

      PUSH(3) PUSH(1) PUSH(2) SUB MULT
      

      The instruction sequence should just leave the result of computing the expression on the top of the stack. Hint: the order of instructions you need to generate is exactly a post-order traversal of the expression tree.

      Test Cases for Compile

      I gave you a number of test cases for Compile in the starter code, but you'll need to use the same spacing in the answers to pass the tests. To ensure that's the case, you will want to include spaces in your output as follows for the binary operator nodes:

      left + " " + right + " ADD"
      

      Note

      Aside: Most modern compilers (including the Sun Java compiler) are implemented using the Visitor Pattern. The compilation process is really just a sequence of visit operations over the abstract syntax tree. Common steps include visitors 1) to resolve the declaration to which each variable access refers; 2) to perform type checking; 3) to optimize the program; 4) to generate code as above; and so on.

  5. Suppose there are n subclasses of Expr, and m subclasses of Visitor. How many classes would have to be added or changed to add a new class to represent division expressions.

  6. Suppose there are n subclasses of Expr, and m subclasses of Visitor. How many classes would have to be added or changed to add a new operation to graphically draw the expression parse tree.

  7. The designers want your advice. Under what circumstances would you recommend using the standard design?

  8. Under what circumstances would you recommend using the Visitor Design Pattern?

Submitting Your Work

Submit your code to the GradeScope assignment named, for example, "Lab 1". You can submit in one of two ways:

  • Upload files: Click "Upload" and select all of your source files, or
  • Link GitHub: Click "GitHub" and select your repository and branch.

Please do not change the names of the starter files. Also:

  • If you worked with a partner, only one of each pair needs to submit the code.
  • Indicate who your partner is when you submit. Specifically, after you upload your files, there will be an "Add Group Member" button on the right of the Gradescope webpage -- click that and add your partner.

Autograding: Gradescope will run an autograder on your code that performs some simple tests. Be sure to look at the autograder output to verify your code works as expected. We will run more extensive tests on your code after the deadline.