Com S 227 --- Introduction to Computer Programming Semantics of Let, Lambda, and Application by Gary T. Leavens 1. AN EXAMPLE What we will do below is to show how Scheme works with Environments to calculate the values of let, lambda and application. The following is a simple example. The problem is to evaluate, in the global environment, the following expression. (let ((two 2)) (let ((add2 (lambda (x) (+ x two)))) (add2 3))) Here we go... Env0: + ~~~> [primitive-procedure | ...| Env0] ... (let ((two 2)) (let ((add2 (lambda (x) (+ x two)))) (add2 3))) To evaluate the LET we need to compute the value of 2 ==> 2 Then evaluate the body in the following environment Env1 (parent Env0): two ~~> [2] (let ((add2 (lambda (x) (+ x two)))) (add2 3)) To evaluate the LET we need to compute the value of (lambda (x) (+ x two)) ==> [procedure|(x)|(+ x two)|Env1] Then evaluate the body in the following environment Env2 (parent Env1): add2 ~~> [procedure|(x)|(+ x two)|Env1] (add2 3) To evaluate the application we first evaluate the operator and its arguments add2 ==> [procedure|(x)|(+ x two)|Env1] 3 ==> 3 then we evaluate the body (of the closure for add2) in Env3 (parent Env1): ;; parent from the closure for add2 x ~~> [3] (+ x two) To evaluate the application we first evaluate the operator and its arguments + ==> [primitive-procedure| ... |Env0] x ==> 3 two ==> 2 ==> 5 2. GENERALIZING FROM THE EXAMPLE The above example shows the general way that Scheme works to do LET, LAMBDA, and application. Let's first look at the overall format of the computation given above. It looks like. Env0: + ~~~> [primitive-procedure | ...| Env0] ... (let ((two 2)) (let ((add2 (lambda (x) (+ x two)))) (add2 3))) Env1 (parent Env0): two ~~> [2] (let ((add2 (lambda (x) (+ x two)))) (add2 3)) Env2 (parent Env1): add2 ~~> [procedure|(x)|(+ x two)|Env1] (add2 3) Env3 (parent Env1): ;; parent from the closure for add2 x ~~> [3] (+ x two) ==> 5 That is, it is a series of pairs of environments and expressions, showing how the computation evolves (down the page). At the end is the ==> and the final value. Now, in the various steps there are subsidiary computations, to compute the values of operators, arguments, and expressions bound by LET, but these all follow the same pattern (recursively) and are (recursively) indented. We don't reproduce the environment for subsidiary computations in the same environment. Take a moment to see what these remarks mean in the above example. We can extract from the above example 3 general rules for moving the computation along to subproblems, and then the next problem. 2.1 LET The rule for LET involves computing the values of the expressions (but not the body) in the original environment, and pose the new problem of finding the value of the body of the LET in a new environment that binds the formal parameters to these values. Envk (parent Envj): y ~~~> [v] (LET ((x1 e1) ... (xn en)) body) To evaluate the LET we need to compute the value of e1 ==> v1 ... en ==> vn Then evaluate the body in the following environment Env(k+1) (parent Envk): x1 ~~> [v1] ... xn ~~> [vn] body ==> value 2.2 LAMBDA The rule for evaluating a LAMBDA expression in an environment is simple. The value is a closure formed from the LAMBDA expression and the environment. Envk (parent Envj): y ~~~> [v] (LAMBDA (x1 ... xn) body) ==> [procedure|(x1 ... xn)|body|Envk] 2.3 APPLICATION The rule for an application involves computing the values of the operator and argument expressions in the original environment. Then one poses the new problem of finding the value of the body of the LAMBDA closure that is the value of the operator expression in a new environment that binds the formal parameters to these values. Envk (parent Envj): y ~~~> [v] (e0 e1 ... en) To evaluate the application we evaluate e0 e1 ... en e0 ==> [procedure|(x1 ... xn)|body|Envi] e1 ==> v1 ... en ==> vn Then evaluate the body (of the closure for e0) in Env(k+1) (parent Envi): x1 ~~> [v1] ... xn ~~> [vn] body ==> value Note where the parent environment Envi comes from. You might also want to compare this rule to that for LET. 2.4 LETREC To be complete we can also give a rule in this same style for LETREC. The rule for evaluating a LETREC in an environment is that we first set up a new environment, whose parent is the original environment (Envk below), with bindings for all the formal parameters (to something unknown, ?? here). Then we evaluate the value expressions, which are typically lambdas, in that environment (not in the surrounding one). Note the environment stored in the closures! Then we bind the formal parameters to the corresponding closures, and evaluate the body in the resulting environment. Envk (parent Envj): y ~~> [v] (LETREC ((x1 (lambda (x11 ... x1m) e1)) ... (xn (lambda (xn1 ... xnm) en))) body) To evaluate the LETREC, we set up a new environment Env1 (parent Envk): x1 ~~~> ?? ... xn ~~~> ?? and in this envrionment we evaluate the expressions that will give the values of the formals to replace the ?? (lambda (x11 ... x1m) e1) ==> [procedure|(x11 ... x1m)|e1|Env1] ... (lambda (xn1 ... xnm) en) ==> [procedure|(xn1 ... xnm)|en|Env1] so that Env1 looks like the following, in which we do the body of the LETREC Env1 (parent Env0): x1 ~~~> [procedure|(x11 ... x1m)|e1|Env1] ... xn ~~~> [procedure|(xn1 ... xnm)|en|Env1] body ==> value