Problem set four is about performing transformations to an abstract syntax tree. While it would be nice for these transformations to reduce execution time, it is essential that the meaning of the program remains unchanged. Formally we can say two functions (programs) f and g are equivalent if for all x, f(x) = g(x). We will see that this idea of equivalence does not always play nicely with the optimizer. So what are some ways we can change a program without changing the results it gives us? One simple way is to rename variables. In programing language theory this is called alpha equivalence. We can formally define alpha equivalence recurisevely: (from here on out I will use x, y, z to represent variables e, e1, e2, en are expressions.) 1) x =alpha x 2) e =alpha e 3) e[z/x] =alpha e'[z/y] => fn x => e =alpha fn y => e' 4) (e1 =alpha e1') and (e2 =alpha e2') => e1 e1' = e2 e2' The above three rules are really all we need. As we know let is simply symbolic sugar for funciton application. Since let x = e1 in e2 is defined as (fn x => e2) e1 it follows let x=e1 in e2 end =alpha let y=e2 in e2' end if (fn x => e2) e1 =alpha (fn x => e2') e1' for example: fn x => x+3 ?=>alpha fn q => q+3 lets try subsituting z for x on the LHS and for q in the RHS (x+3)[z/x] is (z+3) and (y+3)[z/y] is (z+3) By rule 2 these are alpha equivalent therefore the functions are alpha equivalent. We can write proofs like this more conviently by chaning a ^ b ^ c => d to a b c --------- d and putting proofs for a,b and c on little lines above them Example: (fn(x)=>x x)(fn y y) ?=alpha (fn(x)=>x x)(fn y y) can we find a proof to put on top of the line? (Read from the bottom up) axiom 2 -------------------------- z =alpha z -------------------------- q[z/q] =alpha t[z/t] axiom 1 -------------------------- ----------- (fn q=>q) =alpha (fn t=>t) z =alpha z axiom 2 ---------------------------------------- --------------------------------- ((fn q=>q)z) =alpha ((fn t=>t) z) (z+42) =alpha (z+42) ---------------------------------------- --------------------------------- ((fn q=>q)x)[q/x] =a ((fn t=>t) u)[z/u] (y+42)[z/y] =alpha (v+42)[z/v] ---------------------------------------- --------------------------------- (fn x=>(fn q=>q)x) =a (fn u=>(fn t=>t)u) (fn y =>y+42)=alpha(fn v=>v+42) ------------------------------------------------------------------------------ (fn x =>(fn q=>q) x)(fn y => y+42) =alpha (fn u =>(fn t=>t) u)(fn v => v+42) Wow!, that was a lot of formalism, and for something you could probably tell just by staring at the expression. However, proofs like this do have value, because we have broken the idea of variable renaming into a series of short little statements that even a computer can work with. This means it's possible to, say, write a program to help enforce academic integrity rules. There are two other notable equivalencies. Beta equivalence says that can substitue a function's arguement for its formal paramenter. Formally we have (fn x => e)e' =beta e[e'/x] for example (fn x => 1+x) (19+20) =beta (1+(19+2)) another example: (fn x => if true then 1 else x)(1/0) =beta (if true then 1 else (1/0)) (runtime error) eh? (good expression) The notion of beta equivalence comes from "pure" lambda calculus, which has slightly different evalulation semantics than SML. The consequence of this is that just because two ML functions are beta equivalent does not mean that they can be used interchangably. Beta equivalence shows up on ps4 in the form of fucntion inlining. You need to be careful to avoid inlining functions that won't necessarily return. However for functions that always return, beta- reduction/function inlining is a great way to speed up your code. Example: What's an expression that will hang the optimizer? (fn(x)=> x x) (fn(x)=> x x) Lastly we have eta equivalence. (fn x=>e x) =eta e for example val if3 = fn(b: bool, x: 'a, y: 'a) => if b then x else y doesn't act like if because ML functions evaluate their (single) argument before function application, but if is a special form. Here let val u = 0 in if3(u=0, fn x=>x , let val y = 2 div u in (fn x =>x+y) end ) end will divide by zero when we evaluate the third argument to if3. If we replace the second argument with it's eta equivalent we circumvent the problem. let val u = 0 in if3(u=0, fn x => x , fn q => ((let val y = 2 div u in (fn x =>x+y) end) q) ) end Eta equivalence is mostly used as above, to wrap side effects like non-terminiation.