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).
Please discuss the added difficulties of handling recursive types as well (see problem 37). A complete solution for this extension earns you lots of bonus points! Here you need only tell me what would make the solution hard.
let x = 1 in let g = fn y => succ x in let x = 7 in g 0 end end endwhere (let x = M in N end) is shorthand for ((fn x => N) M)? (You can find the translated term in file ~kim/cs334stuff/ML/ML.interp/let.pcf.)
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?
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;
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 of values 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. For practice, first define a default environment default that returns ERROR given any string s.
b. Define a function, update env s t, that, given an environment, env, a string, s representing a variable, and a value, t, returns a new environment that, when applied to s returns t, and otherwise returns what env would have returned. We write this update operation in our rules below as env[s:=t].
Discussion: In this 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.
c. Define a function, newinterp t env, that takes a term, t, (possibly involving free variables) and an environment, env, 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, rho stands for an environment:
(1) (n, rho) >> n for n an integer. (2) (true, rho ) >> true, and similarly for false (3a) (x, rho) >> rho(x), if rho(x) is not a thunk (3b) (x, rho) >> v, if rho(x) is of the form thunk(e,rho') and (e, rho') >> v, otherwise (4) (error, rho) >> error (5) (succ, rho) >> succ, and similarly for the other initial functions (6) (fn x => e, rho) >> closure(x, e, rho)Notice that we save the defining environment along with the formal parameter and function body.
(b, rho) >> true, (e1, rho) >> v (7) -------------------------------------- (if b then e1 else e2, rho) >> v (b, rho) >> false, (e2, rho) >> v (7a) -------------------------------------- (if b then e1 else e2, rho) >> v (e1, rho) >> succ, (e2, rho) >> n (8) ------------------------------------- ((e1 e2), rho) >> (n+1) (e1, rho) >> pred, (e2, rho) >> 0 (9a) ------------------------------------- ((e1 e2), rho) >> 0 (e1, rho) >> pred, (e2, rho) >> (n+1) (9b) ------------------------------------- ((e1 e2), rho) >> n (e1, rho) >> iszero, (e2, rho) >> 0 (10a) ------------------------------------- ((e1 e2), rho) >> true (e1,rho) >> iszero, (e2, rho) >> (n+1) (10a) ------------------------------------- ((e1 e2), rho) >> false (e1, rho) >> closure(x, e3, rhof), (e2, rho) >> v1, (e3, rhof[x:=v1]) >> v (11) ---------------------------------------------------- ((e1 e2), rho) >> vNotice 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, rho[x:=thunk(rec x => e, rho)]) >> v (12) ---------------------------------------- (rec x => e, rho) >> vNotice 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).