CSE 505 Final -- Answer Key


December 13, 1994
110 minutes, open notes, 10 points per question, 140 points total
  1. Compare the notion of a variable in Miranda, Algol-60, Smalltalk, and Prolog.

    A variable in Miranda simply denote a value of some type. As with variables in mathematics, there is no mechanism for changing the value of a variable. Variables in Miranda are either free or bound. Bound variables are mapped to a value; free variables in an expression have no definition within that expression, but must be bound by some lexically enclosing scope. For example, in a function definition, there may be a variable for the parameter of the function, which is bound at the time the function is applied to a value. Variables in Miranda are treated uniformly, no matter whether the variable is bound to a function or a value such as an integer. In Miranda the type of a variable can always be determined statically.

    A variable in Algol-60 is an abstraction of a storage location in a Von Neumann machine. For ordinary types such as integers and booleans, variables are bound to a storage location by a declaration (either of a local variable or a procedure or function parameter). At any point in the program's execution, the value of a variable can be changed by an assignment statement. Variables of type procedure are more limited, in that they can't be assigned to. Except for variables denoting a formal procedure or function parameter, the type of a variable can be determined statically in Algol-60.

    Variables in Smalltalk are similar to those in Algol-60, except that types cannot be determined until runtime. In addition, variables in Smalltalk always contain pointers to their values, rather than sometimes containing the values directly.

    Variables in Prolog are so-called logic variables. A variable can be unified with another variable or term -- in other words, can be constrained to be equal to that other variable or term. Unification is a kind of two-way pattern matching, which is used to solve these kinds of constraints. Unlike Algol-60, there is no assignment statement in Prolog -- the binding of a variable can only be undone by backtracking. Unlike Miranda, an unbound variable can be passed into a goal, or left as part of a structure, perhaps to be unified later with something else.

  2. Consider a typical Algol-60 implementation. Give an example of code where the static link and the dynamic link for an activation record both point to the same activation record; and another example in which where they point to different activation records.

    In the following procedure, when p is called the static and dynamic link will both point to the global stack frame:

    begin integer n; procedure p; begin print(n); end procedure p; n := 2; p; end;

    However, in the following procedure, on each call the static link for p will point to the global stack frame, but for any i not equal to 1, the dynamic link will both point to the frame for another invocation of p:

    begin integer n; procedure p(i: integer); begin if i=10 then print(n) else p(i+1); end procedure p; n := 2; p(1); end;

  3. Suppose we have classes A, B, and C in Smalltalk. B is a subclass of A, and C is a subclass of B. Each contains a method clam, and another method mollusc, as follows: class A clam Transcript show: 'clam here in class A'. self mollusc. mollusc Transcript show: 'mollusc here in class A'. class B clam Transcript show: 'clam here in class B'. super clam. mollusc Transcript show: 'mollusc here in class B'. class C clam Transcript show: 'clam here in class C'. super clam. mollusc Transcript show: 'mollusc here in class C' What happens when you send the clam message to an instance of A? To an instance of B? To an instance of C?

    Here is the output printed to the transcript:

    for an instance of A:

    clam here in class A mollusc here in class A for an instance of B: clam here in class B clam here in class A mollusc here in class B for an instance of C: clam here in class C clam here in class B clam here in class A mollusc here in class C

    Explanation (you don't need to have this explanation in your answer -- just the output is enough):

    When we send clam to an instance of A, we execute the clam method defined in A. This prints 'clam here in class A', and then sends the mollusc message to self (which is an instance of A). We then find the mollusc method in A, and execute it, printing 'mollusc here in class A'.

    When we send clam to an instance of B, we execute the clam method defined in B. This prints 'clam here in class B', and then evalutes super clam. This starts the lookup for clam in A, and finds the clam method there. This prints 'clam here in class A', and then sends the mollusc message to self (which is an instance of B). We then find the mollusc method in B, and execute it, printing 'mollusc here in class B'.

    When we send clam to an instance of C, we execute the clam method defined in C. This prints 'clam here in class C', and then evalutes super clam. This starts the lookup for clam in B, and finds the clam method there. This prints 'clam here in class B', and then evalutes super clam. This starts the lookup for clam in A, and finds the clam method there. This prints 'clam here in class A', and and then sends the mollusc message to self (which is an instance of C). We then find the mollusc method in C, and execute it, printing 'mollusc here in class C'.

  4. Consider the task of simulating a Lisp list in Smalltalk-80. Give the definition for a class ConsCell. Cons cells should understand the following messages:

    How are you representing the empty list? Say why you chose that representation, and compare it with alternative possibilities. Give code that constructs the list (1 2 3), and show how to print each element in the list to the Transcript using do:.

    In the following solution, we define an abstract class ListElement, with two concrete subclasses: ConsCell and EmptyList. Following Common Lisp, we define the car and cdr of the empty list to both be the empty list.

    Class ListElement instance variables: '' car self subclassResponsibility cdr self subclassResponsibility car: c self subclassResponsibility cdr: c self subclassResponsibility do: blk self subclassResponsibility Class ConsCell instance variables: 'car cdr' car ^car cdr ^cdr car: c car := c cdr: c cdr := c do: blk blk value: car cdr do: blk Class EmptyList instance variables: '' car "following Common Lisp, we define the car of an empty list to be the empty list (giving an error would also be a reasonable thing to do)" ^self cdr "following Common Lisp, we define the cdr of an empty list to be the empty list (giving an error would also be a reasonable thing to do)" ^self car: c self error: 'cannot set the car of an empty list' cdr: c self error: 'cannot set the cdr of an empty list' do: blk "don't do anything!" An acceptable alternatives would be to not include the abstract superclass ListElement. An alternative representation for the emtpy list would be to use the existing object nil. You would need to add methods for car, cdr, car:, cdr:, and do: to UndefinedObject (the class of nil). The advantage of the separate class EmptyList is that the messages car, cdr, etc. don't clutter up nil (which is, after all, conceptually a separate notion). The advantage of using nil is that it more closely follows Lisp, and involves defining one less class.

    Here is code that constructs the list (1 2 3), and that prints each element in the list to the Transcript using do:.

    | mylist | mylist := ConsCell new car: 1; cdr: (ConsCell new car: 2; cdr: (ConsCell new car: 3; cdr: EmptyList new)). mylist do: [:x | Transcript show: x printString; cr].

  5. Suppose ConsCell were implemented in Bracha and Griswold's Strongtalk system. Show the generic protocol for ConsCell. generic protocol ConsCell[T] car ^ cdr ^ car: cdr: do: Both ConsCell and EmptyList obey this protocol. If you implemented the empty list as nil, this is still technically legal under Strongtalk, since nil is permitted as a value of any type (although it's a hack).

  6. Regarding the Strongtalk implementation of ConsCell, is "list of integer" a subtype of "list of number"? Is "list of number" a subtype of "list of integer"? Why?

    Neither is a subtype of the other. ConsCell[Integer] isn't a subtype of ConsCell[Number], since, for example, by the contravariant rule Number (as the argument type for car:) would need to be a subtype of Integer. On the other hand, ConsCell[Number] isn't a subtype of ConsCell[Integer], since Number (as the return type for car) would need to be a subtype of Integer.

    To illustrate this, suppose we have a list of integers. If we pass it to a method expecting a list of numbers, this method should be able to set the car of the list to 3.4 (a float) -- but this would violate the type ConsCell[Integer]. On the other hand, if we pass a list of numbers, say (3.45 2 0), to a method expecting a list of integers, this method should be able to get the car of the list, and take its remainder mod 2 (an operation applicable only to integers).

    What would the relation be if Strongtalk used the covariant rule instead of the contravariant rule?

    ConsCell[Integer] would be a subtype of ConsCell[Number],

  7. Suppose you were defining cons cells in SELF instead, following the usual SELF conventions. Describe the objects you would define. Describe the process of making a new list, and of sending it the car message. (In particular describe what messages are delegated to what).

    If we follow the Smalltalk class definitions, more or less, we could define objects ListElement traits, ConsCell traits, ConsCell, and EmptyList. The inheritance hierarchy looks like this:

    Object | ListElement traits / \ ConsCell traits \ | \ ConsCell EmptyList The method for do: would be attached to ListElements traits, ConsCell traits, and EmptyListListElements traits would be a dummy). ConsCell would have slots named car and cdr, and hence would define methods for car, car:, cdr and cdr:. To make a new nonempty list element, we clone ConsCell.

    I didn't separate out the EmptyList traits from the EmptyList object, since there is only one empty list.

  8. Suppose that B1, B2, and B3 have been declared as global variables in Smalltalk-80. One has the following code in a workspace in Smalltalk-80, and selects it all and then "do it" from the menu. | p1 p2 p3 | B1 := [p1 := Point x: 1 y: 1]. B2 := [p1 := Point x: 100 y: 100]. p1 := Point x: 3 y: 3. p2 := Point x: 10 y: 10. p3 := Point x: 33 y: 33. B3 := [p1+p2]. Subsequently, one selects the following code and says "print it". What is printed? Explain your reasoning. | p1 | p1 := B3 value. B1 value. p1 + B3 value

    24@24 is printed. Here's what happens. When the first chunk of code is evaluated, we bind p1 to 3@3, p2 to 10@10, p3 to 33@33, and B1, B2, and B3 to blocks. When the second chunk of code is evaluated, a different variable p1 is created and bound to B3's value, which is (3@3)+(10@10) = 13@13. Then we send B1 the value message, causing p1 to be assigned 1@1 (where p1 is the temporary shared by B1, B2, and B3, and distinct from p1 in the other context). Then we evaluate p1 + B3 value. B3 value is (1@1)+10@10) = (11@11), so p1 + B3 value is (13@13)+(11@11) = 24@24.

  9. There is code in the previous example to create points with values (x=1,y=1), (x=3,y=3), (x=10,y=10). (x=33,y=33). and (x=100,y=100). Exactly when, if ever, in the execution of the code are these points created? Exactly when, if ever, do they become no longer accessible and hence subject to being garbage collected? Again, explain your reasoning.

    3@3 is created when the first chunk of code is evaluated, and is bound to p1. Even though p1 is a temporary, it is referred to by blocks B1, B2, and B3, and since B1, B2, and B3 are globals, it stays around until we evaluate B1 value in the second chunk of code. At this point 3@3 is no longer referenced and can be garbage collected.

    10@10 is created when the first chunk of code is evaluated, and is bound to p2. Again, since B3 refers to it, it isn't garbage collected; and since nothing assigns to p2, it will stay around indefinitely (or until something assigns into the global variable B3).

    33@33 is created when the first chunk of code is evaluated, and is bound to p3. However, since p3 is inaccessible afterwards, this point is garbage collected after the code is evaluated.

    1@1 is created by B1 is sent the message value when the second chunk of code is executed. Since it is then assigned to p1, which is referred to by blocks bound to global variables, it remains indefinitely (or until B1 or B2 is evaluated, or B1, B2, and B3 are re-bound).

    100@100 would be created if B2 were evaluated -- but it never is in the above code sequences.

  10. Suppose that one were writing a denotational semantics for Smalltalk, following the semantics given in Cardelli's paper "A Semantics of Multiple Inheritance". When would the meaning of a Smalltalk expression be 'bottom', and when would it be 'wrong'?

    The meaning of a Smalltalk expression would be 'wrong' if its evaluation would yield a message-not-understood error. It would be 'bottom' if its evaluation yielded an error of some other kind, or didn't terminate.

  11. Consider the CLOS rules for multiple inheritance, given the following class hierarchy. A / \ B C \ / D In other words, B is a subclass of A, C is a subclass of A, and D is a subclass of B and C (where B is listed before C in the DEFCLASS).

    Describe how the search for a method is performed given a message sent to an instance of D.

    CLOS forms a linear ordering of D and its superclasses, using the rule "left to right up to joins". In this case the order is (D B C A). CLOS would thus first check D for a method, then B, then C, then A.

  12. Consider the following Prolog definition of append in Prolog. append([],Ys,Ys). append([X|Xs],Ys,[X|Zs]) :- append(Xs,Ys,Zs). What answers are produced by Prolog for the following queries? Give all of the answers if there is more than one (unless there is an infinite number of answers). ?- append([1,2],[5,6,7],C). C=[1,2,5,6,7] ?- append([1,2],B,C). C=[1,2|B] ?- append([1],[5,6],W). W=[1,5,6] ?- append([1,A],B,[1,2,3]). A=2, B=[3] ?- append([1|A],B,[1,2,3]). A=[], B=[2,3] A=[2], B=[3] A=[2,3], B=[] Note: Quintus Prolog will give answers in terms of internal variable names, so that e.g. instead of just C=[1,2|B], Quintus would say something like B = _9164, C = [1,2|B] Either answer is acceptable, of course.

  13. Briefly, what are the differences between Constraint Logic Programming and Prolog?

    Constraint Logic Programming is a family of programming languages parameterized by the domain D of the constraints. CLP programs can have constraints in the goals and bodies of rules, in addition to other goals as in Prolog. Thus in Prolog, rules are of the form

    P :- Q1, ... Qn while rules in a CLP language are of the form P :- Q1, ... Qn, C1, ... Cm where constraints Ci are over some domain D

    Unlike Prolog, the function symbols for constraints over the domain D are interpreted.

    (Actually, Prolog can be viewed as an instance of a CLP language, where the only constraints are equality constraints over the Herbrand Universe. The "Herbrand Universe" is the domain of values for an ordinary Prolog program.)

  14. More tacky but easy-to-grade true/false questions!

    1. The binding of an exception to an exception handler is done using lexical scoping rules in both Ada and Smalltalk-80.

      False. (It uses dynamic scoping rules.)

    2. Miranda is a higher-order language, since functions can take other functions as arguments, or return them as results.

      True.

    3. The "cut" operator in Prolog is used to prune off branches of the seach tree.

      True.

    4. The search order in Prolog is breadth first.

      False. (It's depth first.)

    5. The CLP(R) implementation of Jaffar et al. uses several constraint solvers that work in combination. When possible the implementation uses a faster and less general solver rather than a general and slower one.

      True.