CS 334
Programming Languages
Spring 2000

Solutions to Assignment 7
Due Thursday, 4/18/2002


  1. Please do problem 10.1 on page 10-43. (If object-oriented languages do not solve all the problems, just say so for those problems.)

    9.7.1: Modules not types. Classes are used as types in OO languages (and interfaces are even better as types), so objects can be classified.

    9.7.2: Modules are static: Classes can be used to generate new objects at run-time. Hence all objects are not known statically.

    9.7.3: Modules that export types do not adequately control operations on variables: In particular, one cannot guarantee that initialization code can be run. This can be guaranteed with OO languages because the object is not created without running the constructor, which does initialization code. [Some modules do allow the programmer to put in initialization code, and so don't suffer from this.]

    9.7.4: Modules do not always adequately represent how they depend on imported types: For example a module in Modula-2 may import a type and depend on it having equality be meaningful, while this may not be the case. C++ is a prime example where a template does not allow the programmer to express dependencies. Ada does support this as do GJ and Eiffel with bounded polymorphism. ML also does a good job with its use of functors. No clear win for OO languages over modules on this one.

    9.7.5: Module definitions include no specification of the semantics of the provided operations: Eiffel and, more recently, Java do provide run-time checked assertions, but these are the only ones I know of. They happen to be OO languages, but there is no clear reason why OO languages should be any better than any other kind about this.

  2. Suppose class B extends class A (i.e., B is a subclass of A), and moreover B contains more instance variables than A. In Java, and most object-oriented languages, B is treated as a subtype of A. Thus an object from class B can be used in any context expecting a value of class A. In particular an object of class B can be assigned to a variable of class A.

    Because B has extra instance variables, it requires more space than A. This does not cause problems in Java because objects are held as implicit references. Thus each variable of object type contains a pointer to the actual instance variables (and indirectly) methods of the object. Thus variables of object types can always be allocated enough space to hold a pointer.

    In C++ objects are not implicit references. That is, a value of type B is not a pointer to space in the heap holding the data of the object. Instead that data is directly stored (on the stack, of course). The program in Figure 10.13 and the discussion on pages 10-27 and 10-28 of the text discuss what happens in this case versus when pointers to objects are used. (Though I thought the discussion was not as clear as it might have been.)

    Let's make the example a bit more compelling by supposing that A has a protected instance variable, x, and that B also has a protected instance variable y of type int (of course, it also inherits x). Suppose also that method q of class A prints out the value of x, while q in class B prints out the values of x and y.

    #include <iostream.h>
    
    class A{
    public:
      int x;
    
      A() {x = 1;}
    
      void p()
      { cout << "A::p\n" ; }
    
      virtual void q()
      { cout << "A::q\n" << x << endl; }
    
      void f() 
      { p(); q(); }
    };
    
    class B : public A
    {
    public:
      int y;
    
      B(){y = 2;}
      void p()
      { cout << "B::p\n"; }
    
      void q()
      { cout << "B::q\n" << y << endl; }
    };
    
    int main()
    { A a;
     B b;
     a.f();
     b.f();
     a = b;
     a.f();
    }
        

    Please explain what would now happen in line (30) of Figure 10.13 when a = b is executed. (In particular, discuss what happens to the instance variables in the object held in b.) What would be printed out as the output from the program. Please explain why these results obtain.

    You are welcome to run the program under a C++ compiler (g++ on our UNIX boxes) to see what happens. To run the program after it has been compiled, just type "a.out" (presuming you didn't assign the compiler output a different name).

    The program prints out:

        A::p
        A::q 
        1
        A::p
        B::q
        2
        A::p
        A::q
        1
        
    What is happening here is that the assignment, a = b, results in the truncation of b's instance variables (the x is copied and the y is lost), while keeping the methods from class A. In fact, if B's constructor had changed the value of x to 17, that would have been printed out by the call of q, making it even clearer what was happening. Of course, this is generally NOT what users would expect, but in C++ it presumes it should do that because that must be what you meant!

  3. Please do problem 10.11 on page 10-44.

    The bottom line on this one is that it is generally OK to make things more public, but not OK to make them more hidden. In particular, subtyping will break if you make public variables non-public in subclasses, but there is no problem making protected variables public (though you might not want to do it for principled reasons of breaking a contract made by the superclass).

    Now issues of changing between private and protected are a bit trickier. If a variable is private, then it may not make sense to make it protected (or public) in a subclass, because the subclass was not even supposed to be able to see it and the programmer of the superclass might not have wanted it to be accessible. Nevertheless it won't cause typing problems if this is possible (though one still might not want to allow it). On the other hand, changing something from protected to private [we're ignoring package visibility here - otherwise we have a problem!] only gets in the way of subtyping in fairly bizarre cases (examples that show problems with shifting public to protected can be shifted to be problems here if those examples are all embedded in subclasses). However, if the programmer of the superclass expected the subclass to override methods that do need to access the formerly protected variable, then there might be problems in effectively overriding the method.

  4. Please do problem 10.19 on page 10-44.

    The code prints:

        B.p
        A.q
        C.q
        B.p
        
    This is just straight dynamic method dispatch.

  5. I know you would feel cheated if we went a week without writing interpreters for PCF, so we'll start writing an interpreter for PCF in Java this week. Next week you'll actually finish it up. To make life simple I'll have you emulate what we did in ML as closely as possible. The main difference is that the datatype definition (e.g., of term) will turn into an interface, while each of the cases of the datatype definition will turn into a class which implements that interface.

    I would like you to write a series of classes to represent the terms of PCF. These will be represented very much like the type term in your environment interpreter for PCF written in ML. Thus each term will represent an abstract syntax tree in Java. Each tag in the term datatype definition in ML will be represented by a distinct class in Java. If the tag comes with components (e.g. AST_IF has three term components), then it will have that number of instance variables with the corresponding types (e.g., class IfExp will have 3 instance variables, and its constructor will take three parameters, each of which represents a PCF expression).

    The datatype term will be represented by the following interface:

       public interface PCFExpression{
          String getStringRep();
          /** Return a printable representation of the term in the object */
       }
       

    Each of the cases of the definition of the datatype term will be represented by a class which implements PCFExpression. Thus there will be classes named IDExp, NumExp,..., FunExp, and AppExp. When a getStringRep() message is sent to one of these expressions, it should return a string representing the original expression.

    For example, suppose we wish to build an object corresponding to the PCF term: if (isZero 3) then 1 else 2. A program to build this might look like:

       PCFExpression three = new NumExp(3);
       PCFExpression isZ = new IsZeroExp();
       PCFExpression cond = new AppExp(isZ,three);
       PCFExpression trueExp = new NumExp(1);
       PCFExpression falseExp = new NumExp(2);
       PCFExpression theTerm = new IFExp(cond,trueExp,falseExp);
       System.out.println(theTerm.getStringRep());
    

    This program would build a representation of the "if" expression. The string printed at the end would look pretty much like the original expression (though it might have extra parentheses).

    The key to all of this is dynamic method invocation. Thus class AppExp will have two instance variables, each of type PCFExpression. At run-time, you have no idea what classes generated the objects held there. However, if you send a getStringRep message to each they should return the appropriate strings representing the expressions.

    Be sure to notice the differences in the organization of your Java-based interpreter (actually just a pretty-printer, but next week ...) and the ML interpreter. The organizational differences between functional and object-oriented approaches are quite important, even though the individual code snippets are not very different.

    In next week's assignment, you will complete this by adding a method called evaluate to the interface and to each of the classes. (To do this you will also need to write classes corresponding to each of the cases of the datatype value of the ML interpreter.) For this week just think about representing the expressions and printing them out.

    As you can see from the program fragment above, entering a PCF expression is very painful. For extra credit (and the eternal gratitude of your classmates!), write a parser for PCF expressions that builds your abstract syntax trees. To see how to do this, look at the parser.sml file that was used with your ML-based interpreters.