CS 334
Programming Languages
Spring 2002

Assignment 4
Due Thursday, 3/7/2002

  1. Please do problem 5.8 on page 5-48 of Louden.

  2. We've seen in ML that functions of types S -> (T -> U) and (S * T) -> U are almost interchangeable, in the sense that any function of one type can be easily rewritten as a function of the other type.

    Show that the ML types S -> (T -> U) and (S * T) -> U are essentially "the same" by defining higher-order ML functions

            Curry:  ((S * T) -> U) -> (S-> (T -> U)) and 
            UnCurry:  (S -> (T -> U))-> ((S * T) -> U)
    such that for all f : (S * T) -> U and g: S -> (T -> U), (i) UnCurry (Curry (f)) = f, and (ii) Curry (UnCurry (g)) = g. This shows a one-to-one correspondence between the two types.

    That is, you must write ML functions Curry and UnCurry with types as above and then "prove" that the two equations above always hold for your functions and any f and g of the appropriate types. The "proof" is most easily done by applying both the left-hand and right-hand sides of the equation to a general term of the appropriate type (e.g., for (i) apply it to a pair (s,t): S*T) and showing that they both give the same answer. Thus, to prove (i) show UnCurry(Curry (f)) (s,t) = f (s,t) by expanding the definitions of UnCurry and Curry that you are providing. (Note that you can't compute with types -- function bodies are required!)

  3. Problem 6.4, page 6-49 of Louden. Be sure to explain your answers.

  4. Problem 6.22, page 6-52 of Louden. Be sure to explain your answers.

  5. a. What value does your interpreter return in evaluating:
               x = 1
                  g = fn y => succ x
                     x = 7
                     g 0 
    Recall that the parser translates (let x = M in N end) into ((fn x => N) M).

    b. What is the correct answer (independent of your interpreter) for the case in which static scoping is desired.

    c. What is the correct answer for dynamic scoping?

  6. . This program builds on the last problem from the previous assignment. Now we will see how to program an interpreter in a much more efficient manner. No practical interpreter uses explicit substitution of code for efficiency reasons. Most interpreters instead use the idea of a closure to implement substitution.

    Define the datatypes

        datatype value = NUM of int | BOOL of bool | SUCC | PRED | 
                         ISZERO | CLOSURE of (string * term * env) | 
                         THUNK of term * env |  ERROR         
        withtype env = (string * value) list;

    where env represents the type of environments. The withtype construct allows one to define a datatype with a mutually recursive type definition. Environments are one way of encoding substitution---the bindings of free variables in a PCF term are given by an environment. The type value represents the final answers, or values, returned by PCF programs. The first five are self-explanatory. The sixth value, a closure, is the representation for functions: the first part of a closure is the formal parameter, the second part is the body of the function, and the third part is an environment that gives meaning to the free variables in the body of the function. A THUNK is a way of suspending evaluation of a term (similar to a closure), which involves saving the term and its current environment.

    a. An environment is represented as a list of pairs of the form (id,val) where id is the name of the identifier and val is its value in the environment. Thus,

       [("x",NUM 12),("flag",BOOL true)}
    represents an environment where identifier x has value NUM 12 and flag has value BOOL true.

    Define a function, getVal id ev, that, given a string, id representing a variable, and an environment, ev, returns the value associated with id in ev, if there is one. Otherwise it should return ERROR.

    Also define a function update ev id newVal, that takes an environment, ev, an identifier, id, and value, newVal, and returns an updated environment which is identical to ev except that id is associated with newVal. The easiest way to construct this new environment is simply to add the new pair to the beginning of the list corresponding to ev. If getVal always returns the first binding for an identifier, then old bindings do not need to be removed when new ones are added. We write this update operation in our rules below as ev[id := newVal].

    Discussion: In the interpreter defined in part c, applying a function to an argument will result in an updated environment (the formal parameter will be associated with the value of the actual parameter in the updated environment). The function body will then be evaluated in this new environment. The only question is, which environment should be updated to reflect the actual parameter? Since we are considering statically scoped languages here, the environment to be updated is the one in effect when the function was defined, rather than the one in place when the function is called! Since we don't have many names floating around here it may be difficult for you to appreciate this distinction. However, look at the following example:

            let f = fn x => (iszero (succ x))
                in let g = fn y => f y
                in let f = fn x => (iszero x)
                in g 0

    where (let x = M in N) is shorthand for ((fn x => N) M). This should evaluate to false under static scoping (the f in the definition of g is the outermost f), while it returns true under dynamic scoping (the f in the definition of g refers to the most recently defined value of f - the innermost one). Make sure your interpreter evaluates this expression (after you have gotten rid of all of the let's) properly.

    b. Define a function, newinterp t ev, that takes a term, t, (possibly involving free variables) and an environment, ev, that gives meaning to all the free variables in the term, and returns a value representing the evaluation of the term in that environment. (Note: In the initial call to evaluate a term, you will want to pass your default environment from above. Nevertheless, the function should still take two arguments, since in recursive calls---particularly with applying functions---you will want to call newinterp with different environments. See rule 11 below.) In the rules below, ev stands for an environment:

    (1) (n, ev) => n  for n an integer.
    (2) (true, ev ) => true, and similarly for false
    (3a) (x, ev) => ev(x), if ev(x) is not a thunk
    (3b) (x, ev) =>  v, if ev(x) is of the form thunk(e,ev') and 
            (e, ev') => v, otherwise
    (4) (error, ev) => error
    (5) (succ, ev) => succ, and similarly for the other initial functions
    (6) (fn x => e, ev) => closure(x, e, ev)
    Notice that in closures we save the defining environment along with the formal parameter and function body.
           (b, ev) => true,        (e1, ev) => v     
    (7a)    ---------------------------------------      
              (if b then e1 else e2, ev) => v       
           (b, ev) => false,       (e2, ev) => v
    (7b)   ---------------------------------------
              (if b then e1 else e2, ev) => v
           (e1, ev) => succ,       (e2, ev) => n
    (8)    ---------------------------------------
              ((e1 e2), ev) => (n+1)
            (e1, ev) => pred,       (e2, ev) => 0           
    (9a)    ----------------------------------------
               ((e1 e2), ev) => 0                      
            (e1, ev) => pred,      (e2, ev) => (n+1)
    (9b)    ------------------------------------------
               ((e1 e2), ev) => n
            (e1, ev) => iszero, (e2, ev) => 0
    (10a)   -------------------------------------
               ((e1 e2), ev) => true               
            (e1,ev) => iszero, (e2, ev) => (n+1)
    (10b)   ---------------------------------------
               ((e1 e2), ev) => false
            (e1, ev) => closure(x, e3, evf),  (e2, ev) => v1, 
                        (e3, evf[x:=v1]) => v
    (11)    ----------------------------------------------------
                        ((e1 e2), ev) => v
    Notice that the body of the function is interpreted in the environment from the closure, updated to reflect the assignment of the actual parameter value to the formal.
            (e, ev[x:=thunk(rec x => e, ev)]) => v
    (12)    ----------------------------------------
                    (rec x => e, ev) => v
    Notice that in evaluating a recursive expression we simply evaluate the body in an environment in which the recursive name stands for the recursive expression. It is important to put this in the environment so that other recursive calls can be evaluated properly. We use thunks to suspend the evaluation of the call so that it will be not be evaluated until it is needed. When the thunk is encountered, the term can be evaluated in the environment stored with it (as in rule 3).

Back to:
  • CS 334 home page
  • Kim Bruce's home page
  • CS Department home page
  • kim@cs.williams.edu