Environment Model

Practical Session - Week #6

In this session, we will:

Introduction

Q: we already learned about the “Substitution” evaluation model, why do we need another one?

The environment model is an optimization of the substitution applicative model of the operational semantics. It changes the way we map variables to their values.

Q: Why do we need to nest environments?

We do not have a single frame that contains all the variables defined in the body of a closure - instead we may need to lookup variables in other frames. This is because we construct a new frame only when we enter a new scope. A new scope is entered when the interpreter enters a let-exp or when it applies a closure. At this point, the expression we execute in the current scope may refer to variables that come from the enclosing scope - according to lexical scoping rules.

Consider this program:

(let ((x 1))                ;; Enter scope E1
  (let ((y (+ x 1)))        ;; Enter scope E2
    (+ (* y y) x)))

When we compute the body of the inner-let (+ (* y y) x), variable y is defined in the first frame accessible (head of E2), and variable x is defined in the second frame accessible (head of E1). E1 has E2 as a tail.

Consider this program:

(let ((x 1))                ;; Enter scope E1
  (let ((x (+ x 1)))        ;; Enter scope E2
    (+ x x)))    

when we compute the body of the inner-let the variable x that was defined in E1 is hidden by the variable x that was defined in E2

Q: Why do we use the closure environment and not the current environment when applying a closure to obtain lexical scoping?

At every step of the computation of an expression, the evaluation function has access to a parameter env which represents the current environment.

When we apply a closure to arguments, we ignore the current environment, and instead evaluate the body of the closure in an environment which extends the environment stored in the closure with a frame that maps the parameters of the closure to the arguments on which it is applied.

Consider this snippet:

(define make-adder
  (lambda (c)
    (lambda (x) (+ x c))))

(let ((a3 (make-adder 3)))  ;; E1
  (let ((c 1))              ;; E2
    (a3 2)))

We observe in this program a risk that the variable c which appears free in the body of the closure returned when computing (lambda (x) (+ x c)) could be captured in the let of E2.

How does this program work in the substitution model

applicative-eval[ (let ((a3 (make-adder 3)))
                     (let ((c 1))
                        (a3 2))] ==>

applicative-eval[ ((lambda (a3) (let ((c 1)) (a3 2))) (make-adder 3))] ==>

   applicative-eval[ (lambda (a3) (let ((c 1)) (a3 2))) ] ==>
      closure <a3> (let ((c 1)) (a3 2))) ==>

   applicative-eval[ (make-adder 3) ] ==>
      applicative-eval[ make-adder ] ==> <closure <c> (lambda (x) (+ x c))>
      applicative-eval[3] ==> 3
   ==>
   applicative-eval[(lambda (x) (+ x 3))] ==> <closure (x) (+ x 3)>

applicative-eval[(let ((c 1)) (<closure (x) (+ x 3)> 2))] ==>
applicative-eval[((lambda (c) (<closure (x) (+ x 3)> 2)) 1)] ==>
   
   applicative-eval[(lambda (c) (<closure (x) (+ x 3)> 2)] 
      ==> <closure (c) (<closure (x) (+ x 3)> 2)>
   applicative-eval[1]
      ==> 1
   ...
   applicative-eval[ (+ 2 3) ] ==>
      applicative-eval[+] ==> <prime-op +>
      applicative-eval[2] ==> 2
      applicative-eval[3] ==> 3
==> 5

How does it work in the environment model

(let ((a3 (make-adder 3))) ;;E1
  (let ((c 1))             ;;E2
    (a3 2)))               ;;E3

when evaluating the body of the inner let (a3 2):

If we use the current environment, we will use E2 to evaluate c in (lambda (x) (+ x c)) and get (+ 2 1)

If we use the closure’s environment, we will use E1 to evaluate c in (lambda (x) (+ x c)) and get (+ 2 3)

Environment Diagrams

We exercise the drawing of environment diagrams in the box-env model presented in class.

Definitions

When a define expression is evaluated, a new binding is added to the GE single frame. image:1_1.PNG

image:1_2.PNG

Env Diagram Example 1: Recursion

image:2_1.PNG

Notice how the closure is pointing to the GE, and so each call to this closure (including recursive ones) will extend the GE. The depth of the recursion is represented by the control links. Successive recursive calls return in sequential order (Ei+1 control link points to Ei).

Env Diagram Example 2: Definition and application

Consider another example:

image:3_1.PNG

image:3_2.PNG

Env Diagram Example 3: Definition and Let

When we encounter a “let” expression, we can convert it to a lambda expression which we already know how to handle.

image:4_1.PNG

image:4_2.PNG

Q: which environments and which procedures will remain in the end of the evaluation?

The closures f and p will remain. E3 will finish its evaluation and return to E2, which will finish and return to E1, which will finish and return to the GE. E1, E2, E3 will disappear since no accessible data structure refers to them anymore.

Env Diagram Example 4: Inferring code from a diagram

What program will generate the following environment diagram?

image:PS6_code_restoring.JPG

We can notice that the structure is serial, which implies nested applications of procedures.

Each environment extends the previous one.

The following code will produce the environment diagram from above:

(define x 5)

(let ((a 2))
  (let ((b 3))
     (let ((c 4))
        (+ x a b c))))

Pair Data Structure using Closures

Env Diagram Example 4: lazy procedural pair implementation

Part A:

image:5_1.PNG

image:5_2.PNG

Notice p1 is a closure expecting a selector procedure. When we call p1, the env we extend will be E1, which is why we see x, y.

Part B: We will display a call to p1 with a selector:

image:6_1.PNG

Watch the evaluation process:

What line of code needs to be added to produce this diagram?

Accordingly, the env diagram for this evaluation is:

(p1 (lambda (a b) a))

Q: which env and closures will remain at the end of the evaluation?

set!

set! is a way to change the value of a variable. for example :

>(define x 5)
>x
5
>(set! x 80)
>x
80

Box

Boxing in scheme is required to wrap a variable in a box, so we can access it through the box, but also change the value inside that box. A natural way to look at boxes is like pointers, which we can access data through but the data inside can be changed.

In the box environment model, we use boxing to enable mutation. We saw in the lecture why we needed mutation to properly model recursion (letrec) and model the global environment with forward usage of global variables and global mutual recursive procedures.

In the box environment model variables are bound to boxes that contain a value, instead of mapping variables directly to values. In addition, the global environment is modeled in a special way that allows addition of a binding (with a method add-binding!).

Motivation: we want to support mutation in the language we implement. Note that at this point, all the variables are immutable - we can create a new variable and bind it to a value, but we cannot change its value. In some cases, we want to be able to change the value of an existing variable.

Example: a counter.

1) How do we do this in Scheme?

We use the box data type which has the following interface:

(box v)
(box? x)
(unbox b)
(set-box! b v)

Box works like a pointer to an existing variable.

Example application - let us create a counter object.

(define make-counter
  (lambda ()
    (let ((count (box 0)))
      (lambda (msg)
        (cond ((eq? msg 'get) (unbox count))
              ((eq? msg 'inc!) (set-box! count (+ 1 (unbox count))))
              ((eq? msg 'reset!) (set-box! count 0))
              (else (error "unknown message to counter")))))))

This counter works with the same pattern as the make-pair example we saw in class: it is a closure factory with a message dispatch body. The functional interface of the counter is:

;; Type: [Counter -> Number]
(define counter-get
  (lambda (counter) (counter 'get)))

;; Type: [Counter -> Void]
(define counter-inc!
  (lambda (counter) (counter 'inc!)))

;; Type: [Counter -> Void]
(define counter-reset!
  (lambda (counter) (counter 'reset!)))

How to use a counter in code:

;; Count how many odd numbers there are in a given tree.
(define count-odds
  (lambda (tree)
    (let ((counter (make-counter)))
      (letrec ((loop (lambda (tree)
                       (cond ((empty? tree) (void))
                             ((pair? tree) (loop (car tree)) (loop (cdr tree)))
                             ((and (number? tree) (odd? tree)) (counter-inc! counter))))))
        (loop tree)
        (counter-get counter)))))
;; Variant of the counter object: functional selector instead of message dispatch (lazy selector)

;; [Empty -> Counter]
;; Counter is implemented as: [(Box(Number) -> Any) -> Any]
(define (make-counter-sel)
  (let ((count (box 0)))
    (lambda (sel)
      (sel count))))

;; Implement counter interface:

(define counter-sel-get
  (lambda (counter-sel) 
    (counter-sel (lambda (b) (unbox b)))))

(define counter-sel-inc!
  (lambda (counter-sel) 
    (counter-sel (lambda (b) (set-box! b (+ 1 (unbox b)))))))

(define counter-sel-reset!
  (lambda (counter-sel) 
    (counter-sel (lambda (b) (set-box! b 0)))))

Adding set! to our language

There are 3 steps we should take to add set! to our language:

Syntax definition:

<cexp> ::= ... | ( set! <var> <cexp>)            // SetExp(var: varRef, val: CExp)

Adding type definitions for set!:

export interface SetExp {tag: "SetExp", var: VarRef; val: CExp; }

export const makeSetExp = (v: VarRef, val: CExp): SetExp =>
    ({tag: "SetExp", var: v, val: val});

export const isSetExp = (x: any): x is SetExp => x.tag === "SetExp";

Update the Compound disjoint union type:

export type CompoundExp = AppExp | IfExp | ProcExp | LetExp | LitExp | LetrecExp | SetExp;

export const isCompoundExp = (x: any): x is CompoundExp =>
    isAppExp(x) || isIfExp(x) || isProcExp(x) || isLitExp(x) || isLetExp(x) ||
    isLetrecExp(x) || isSetExp(x);

Modify the function parseL4SpecialForm

export const parseL4SpecialForm = (op: Sexp, params: Sexp[]): Result<CExp> =>
    isEmpty(params) ? makeFailure("Empty args for special form") :
    op === "if" ? parseIfExp(params) :
    op === "lambda" ? parseProcExp(first(params), rest(params)) :
    op === "let" ? parseLetExp(first(params), rest(params)) :
    op === "quote" ? parseLitExp(first(params)) :
    op === "letrec" ? parseLetrecExp(first(params), rest(params)) :
    op === "set!" ? parseSetExp(params) :
    makeFailure("Never");

Add the parsing functions for SetExp

const parseSetExp = (params: Sexp[]): Result<SetExp> =>
    isEmpty(params) ? makeFailure("set! missing 2 arguments") :
    isEmpty(rest(params)) ? makeFailure("set! missing 1 argument") :
    ! isEmpty(rest(rest(params))) ? makeFailure("set! has too many arguments") :
    parseGoodSetExp(first(params), second(params));

const parseGoodSetExp = (variable: Sexp, val: Sexp): Result<SetExp> =>
    ! isIdentifier(variable) ? makeFailure("First arg of set! must be an identifier") :
    bind(parseL4CExp(val), (val: CExp) => makeOk(makeSetExp(makeVarRef(variable), val)));

env-eval

const evalSet = (exp: SetExp, env: Env): Result<void> =>
    bind(applicativeEval(exp.val, env), (val: Value) => 
        bind(applyEnvBdg(env, exp.var.var), (bdg: FBinding) =>
          makeOk(setFBinding(bdg, val))
        )
    )