CSE 505 Final -- Answer Key

December 12, 1997


110 minutes, open notes, 125 points total.
  1. (5 points) Consider the following claim. Is this correct? Discuss.

    No, this is not correct. Representing the user's input and the system's output as lists of characters works just fine with lazy evaluation, except for the problem of synchronizing them. However, if we are doing eager evaluation, when we try to pass the list of input characters to a function Eager Haskell would want the entire list, even though the user hadn't typed all of them yet. Similarly, any output function would want the entire list of output characters.

  2. (8 points) Suppose the following Haskell code for trees has been loaded. -- the tree datatype data Tree a = EmptyTree | Node a (Tree a) (Tree a) -- various tree traversal functions preorder EmptyTree = [] preorder (Node x left right) = [x] ++ preorder left ++ preorder right inorder EmptyTree = [] inorder (Node x left right) = inorder left ++ [x] ++ inorder right postorder EmptyTree = [] postorder (Node x left right) = postorder left ++ postorder right ++ [x] -- some sample trees t1 = Node 5 (Node 2 EmptyTree EmptyTree) (Node 8 EmptyTree EmptyTree) t2 = Node "squid" EmptyTree (Node "tuna" EmptyTree EmptyTree) What is the result of evaluting the following Haskell expressions? Give both the value and its type. If there is a compile-time type error, or a non-terminating computation, say so. If the result is an infinite data structure, give some initial parts of it. postorder: <> :: Tree a -> [a] preorder t1: [5, 2, 8] :: [Int] Node 100 t1 t1: (Node 100 (Node 5 (Node 2 EmptyTree EmptyTree) (Node 8 EmptyTree EmptyTree)) (Node 5 (Node 2 EmptyTree EmptyTree) (Node 8 EmptyTree EmptyTree))) :: Tree Int Node 100 t1 t2: type error

  3. (10 points) Suppose that you have defined a class Complex of complex numbers in Smalltalk, with instance variables rPart and iPart for the real and imaginary parts. Show the appropriate methods so that you can provide a class-specific initialization message, so that you can write Complex r: 3.4 i: 5.0 to create a new complex number. Describe how the expression Complex r: 3.4 i: 5.0 is evaluated (in particular, any metaclasses and where the methods are found).

    We would add the following method to Complex class (i.e. to the metaclass for Complex):

    r: newR i: newI ^self new setR: newR setI: newI and another method to Complex: setR: newR setI: newI r := newR. I := newI. When we evaluate Complex r: 3.4 i: 5.0 we send the message r:i: to the class Complex with the arguments 3.4 and 5.0. As always, we look in the receiver's class for a method, so we look in the metaclass for Complex and find the r:i: method just defined. "self new" in the code sends the metaclass the method "new", which creates a new instance of Complex. We then send the new instance the message "setR:setI:". This looks in its class, namely Complex, to find the method that sets the r and i parts. We then return the initialized complex number.

  4. (10 points) Describe how complex number creation and initialization might be handled in a prototype-based system with inheritance.

    We might have a prototypical complex number, with 0.0 in the r and i slots, along with other slots with methods for +, print, and so forth. To make a new complex number c, we make a descendant of the prototypical complex number, copying down the r and i slots (but not the others). When a message is sent to c, we first look in its slots for a method; if none is found, then its parent's slots, and so forth. Thus, the r and i slots would be found locally in c, while the methods for + and print would be found in the parent.

  5. (10 points) Why is the lookup of an exception handler in Smalltalk done using dynamic rather than lexical scoping?

    Lookup of an exception handler is done using dynamic lookup since which exception handler is appropriate will depend on the context in which the erroneous method is called. For example, if x/y is being evaluated in a method m1, and the division raises a divide by zero exception, we want to look for an exception handler in m1.

  6. (9 points) This question concerns abstract types and subtypes, parameterized types, and the contravariant rule. Suppose we have an abstract type Dictionary parameterized by K, the types of the keys, and by V, the types of the values. Dictionary has two operations: lookup, which takes a single argument of type K and returns an object of type V; and store, which takes two arguments of types K and V respectively. Thus lookup looks up a key in the dictionary and returns the corresponding value, while store enters a new key-value pair in the dictionary, or replaces the value in an existing key-value pair.

    We also have an abstract type Array, parameterized by a single type T. Array also has two operations: lookup, which takes a single argument of type integer and returns an object of type T; and store, which takes two arguments of types integer and T respectively.

    Thus, an array is a kind of dictionary where the keys are restricted to being integers.

    Using the contravariant rule, what is the subtype relation between the following pairs of types? The answer to each question would be either X is a subtype of Y, Y is a subtype of X, they are equivalent (i.e. each is a subtype of the other), or neither is a subtype of the other.

    1. Dictionary[Object,Object] and Array[Object]
      Dictionary[Object,Object] is a subtype of Array[Object]
    2. Dictionary[Integer,Object] and Array[Object]
      Dictionary[Integer,Object] and Array[Object] are each subtypes of each other.
    3. Array[Integer] and Array[Object]
      Neither is a subytpe of the other.

  7. (9 points) The same as the previous question, except use the covariant rule. If the covariant rule gives an incorrect answer, give an example of a program that breaks; or one that will always execute without type errors, even though the covariant rule says it is incorrect.
    1. Dictionary[Object,Object] and Array[Object]
      Covariance says Array[Object] is a subtype of Dictionary[Object,Object], but this isn't right. Here is an example of a program that gives an error (in some random language): d : Dictionary[Object,Object]; x : Object d := new Array[10]; /* legal by the covariant rule */ /* the following statement typechecks correctly, but has a type error, since we are trying to use a string as an array index */ d.store('clam',5).
    2. Dictionary[Integer,Object] and Array[Object]
      Dictionary[Integer,Object] and Array[Object] are each subtypes of each other.
    3. Array[Integer] is a subtype of Array[Object]
      This isn't right. Here is an example of a program that gives an error (in the same random language): a : Array[Object]; b : Array[Integer]; b := new array(10); a := b; /* this statement is legal, but results in a clam in our array of integers */ a.store(1,'clam');

  8. (10 points) Describe how Java handles the covariance/contravariance question. Consider both ordinary classes and arrays.

    Hint: Java supports overloading, so that a method

    void mytest(Object x) is considered to be different from a method void mytest(String x) since one takes a parameter of type Object and the other a parameter of type String. In other words, if you have a method void mytest(Object x) in a class A and a method void mytest(String x) in a subclass of A, the void mytest(String x) method won't override the inherited one -- it's considered to be a different method. Also, it is an error in Java to have two methods with the same name and argument types but with different return types.

    The covariance/contravariance question doesn't arise for ordinary classes, since the methods with separate signatures are simply considered to be different, and neither orverrides the other. Subtyping for arrays is handled using the covariant rule; to preserve type safety Java includes runtime type checks.

  9. (10 points) Consider the following example of using F-bounded polymorphism in Pizza. (This is similar to the one in the lecture notes, except that we're allowing mutable sets here.) interface Comparable<A> { boolean less(A x); } interface Set<A implements Comparable<A>> { boolean contains(A x); /* test if a set contains x */ void add(A x); /* add x to the set */ } Add a specification for a method max for Comparable that finds the maximum of the receiver and the argument. Add specifications for a method add_all for Set that adds all of the elements in the argument (also a set) to the receiver, and another method maximum that finds the maximum element in a set.

    interface Comparable<A> { boolean less(A x); A max(A x); } interface Set<A implements Comparable<A>> { boolean contains(A x); /* test if a set contains x */ void add(A x); /* add x to the set */ void add_all(Set<A implements Comparable<A>> s); A maximum(); }

  10. (10 points) In the previous question, why did we need a parameter to the Comparable interface? What would be wrong with just writing interface Comparable { boolean less(Comparable x); } Give an example where this doesn't work right.

    Suppose that both Integer and String implement the Comparable interface. With the F-bounded polymorphic type, we can check that an integer is less than an integer and a string is less than a string, but trying to see if a string is less than an integer doesn't type check. However, in the version that doesn't use F-bounded polymorphism, string less than integer would type check correctly.

  11. (10 points) This question should test your understanding of blocks in Smalltalk, in particular scope issues and returning from blocks.

    Consider the following Smalltalk code. There is a class named Squid, with three methods named method1, method2, and method3.

    Class Squid method1 | x | Transcript show: 'entering method 1'. x := 1. self method2. Transcript show: 'returning from method 1'. method2 | x | Transcript show: 'entering method 2'. x := 2. self method3: [Transcript show: 'x =' , x printString. ^nil]. Transcript show: 'returning from method 2'. method3: b | x | Transcript show: 'entering method 3'. x := 3. b value. Transcript show: 'returning from method 3'. If we evaluate s method1 for an instance s of Squid, what is printed to the transcript? Explain your reasoning.

    The following is printed:

    entering method 1 entering method 2 entering method 3 x = 2 returning from method 1 We first execute method1, which prints 'entering method 1' and then calls method2. method2 prints 'entering method 2' and calls method3 with the block as an argument - the nonlocal variable x in the block is bound to the x in method2, which has value 2. We execute method3, which prints out 'entering method 3'. It then sends value to the block. The block prints out 'x=2' (since the x is the one from method2), and then returns. This immediately returns to where method2 was called in method1. So we print 'returning from method 1' and terminate.

  12. (10 points) For a conditional statement, Kaleidoscope requires that the truth or falsity of the condition be known at the time the conditional is executed. Thus if we define the following version of not: constructor my_not (b : boolean) = (c : boolean) if b then c=false else c=true; end constructor my_not; we can use it to find the value of x in the following equality constraint: x = my_not(false); but we can't use it to find the value of y here: true = my_not(y); Suppose Kaleidoscope were changed to remove this restriction, so that we wouldn't need to know the value of the conditional when it was encountered, and so we could find an appropriate value for y to satisfy the constraint. Briefly discuss the implications for the language semantics and implementation. (One paragraph is enough.)

    In general the true and false branches of the conditional might contain any Kaleidoscope code, including assignment statements, input/output statements, and others with side effects. If the truth or falsity of the conditional weren't known, the runtime would need to checkpoint the current state, choose one of the branches arbitrarily, and backtrack if the resulting constraints were unsatisfiable. This is potentially costly, and it wouldn't function correctly with side effects that can't be undone (for example, getting input from the user).

    The situation is a bit better for conditionals within a constraint constructor, since a constraint constructor can't have non-local side effects. (Note: I don't expect that anyone's answer will mention this point, since it wasn't emphasized in class.) So in a constructor we'd still need to do a search, but it would be possible to checkpoint the current state for backtracking.

  13. (14 points) Tacky true/false questions!
    1. Suppose we have an instance m of a Smalltalk class C, and we are evaluating m display. If we encounter the expression super display in the method for display, we'll look for a display method starting in C's immediate superclass.

      False. The method lookup starts in the superclass of the class that has the display method being run (which might not be the same as C).

    2. In CLP(R) the assignment statement X := X+5 is represented by temporarily asserting the constraint Temp = X+5, then retracting the constraint using backtracking, and finally asserting X = Temp.

      False. CLP(R) doesn't have any assignment statement.

    3. Consider the simple language described in Cardelli's paper "A Semantics of Multiple Inheritance". Suppose that the value of an expression e is v. If we apply the typechecking function to e, it will return a type that is the same as or a subtype of the actual type of v.

      False. Actually the typechecking function will return a type that is the same as or a supertype of the actual type of v.

    4. Pizza is statically typed, eliminating the need for any runtime type tests.

      False. Pizza is statically typed (by any but the most fussy of definitions of statically typed), but array subtyping still requires runtime type tests.

    5. Suppose that we had a version of Haskell that uses eager evaluation. If evaluating an expression e in Eager Haskell terminates without error and gives a value v, evaluating the same expression in normal Haskell will always terminate without error and give the value v.

      True.

    6. The converse: If evaluating an expression e in normal Haskell terminates without error and gives a value v, evaluating the same expression in Eager Haskell will always terminate without error and give the value v.

      False.

    7. CLP(R) in effect uses call-by-constraint for argument passing, where equality constraints are set up between the corresponding actual and formal parameters.

      True.