Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
326 views
in Technique[技术] by (71.8m points)

scheme - Understanding the environment model of evaluation

Exercise 3.20 in SICP:

Draw environment diagrams to illustrate the evaluation of the sequence of expressions

(define x (cons 1 2))
(define z (cons x x))


(set-car! (cdr z) 17)

(car x) 17

using the procedural implementation of pairs given above.

My eyes are destroyed so I cannot draw. I will instead try to imagine as best as I can how the environment model evolves.

First, here is the procedural pairs implementation.

(define (cons x y)
  (define (set-x! v) (set! x v))
  (define (set-y! v) (set! y v))
  (define (dispatch m)
    (cond ((eq? m 'car) x)
          ((eq? m 'cdr) y)
          ((eq? m 'set-car!) set-x!)
          ((eq? m 'set-cdr!) set-y!)
          (else (error "Undefined 
                 operation: CONS" m))))
  dispatch)

(define (car z) (z 'car))
(define (cdr z) (z 'cdr))

(define (set-car! z new-value)
  ((z 'set-car!) new-value)
  z)

(define (set-cdr! z new-value)
  ((z 'set-cdr!) new-value)
  z)

Each has a pointer to the global environment.

Here is the interactions and my solution as I imagined it.

(define x (cons 1 2))

apply cons
cons created environment called e1 - the global environment is the enclosing environment
x bound to 1
y bound to 2
set-x!, set-y! and dispatch each has a pointer to e1
dispatch is bound to the name x in the global environment

(define z (cons x x))

apply cons
cons create e2 - global is enclosing
x bound to x with respect to global (shared)
y bound to x with respect to global (shared)
set-x!, set-y! and dispatch each has a pointer to e2
dispatch is bound to the name z in the global environment

(set-car! (cdr z) 17)

apply set-car!
set-car! creates e3 - global is enclosing
z is bound to (cdr z) with respect to global
apply cdr
cdr creates e4 - global is enclosing
z is bound to z with respect to global
dispatch creates e5 - e2 is enclosing
m is bound to 'cdr
return x with respect to e2.
x shares x with respect to global (as well as x with respect to e1)
back to e3
new-value is bound to 17
dispatch creates e6 - e2 is enclosing
m is bound to 'set-car!
set-x! with respect to e2 is returned
apply set-x!
set-x! creates e7 - e2 is enclosing
new-value is bound to 17
set x with respect to e2 to 17
since x is e1, x in e2 and x in global share a procedure object that has a pointer to e1, the procedure object's car is changed.

I hope this is understandable. Is my solution correct?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Here's the answers to your specific questions.

I rename the global variable x to v to avoid confusion.

 (define v (cons 1 2))

apply cons
cons created environment called e1 - the global environment is the enclosing environment
x bound to 1
y bound to 2
set-x!, set-y! and dispatch each has a pointer to e1
dispatch is bound to the name v in the global environment

Correct.

 (define z (cons v v))

apply cons
cons creates e2 - global is enclosing
x is bound to v with respect to global (shared)
y is bound to v with respect to global (shared)
set-x!, set-y! and dispatch each has a pointer to e2
dispatch is bound to the name z in the global environment

Correct.

 (set-car! (cdr z) 17)

apply set-car!
set-car! creates e3 - global is enclosing

No.

(in the following, no code formatting markdown is used, to keep the noise to the minimum for the visually-impaired).

First, (cdr z) is evaluated. It is equivalent to (z 'cdr). z is bound to dispatch from e2 frame. This dispatches to e2's y, having received the message 'cdr. This accesses the y slot in e2 environment frame, which holds the value of v from the global environment.

Next, (set-car! v 17) is evaluated. It is equivalent to ((v 'set-car!) 17). v is bound to dispatch of the e1 frame. Having received the 'set-car! message, it dispatches to e1's set-x! function. This will thus call (set-x! 17) with the e1's set-x!. This in turn calls (set! x 17) within the environment frame e1. Thus it accesses - and modifies - the "x" slot in the environment frame e1!

From now on, any future operation with v will reflect this change, because v refers to the frame e1, and that frame was changed. e1 frame's stored value under "x" is no longer 1, but 17.

No new environment frames are created by accessing these values. The hidden frames referred to by the values are accessed, and possibly modified.

Only cons creates new hidden environment frames which get attached to the newly created "cons" values (i.e. to the dispatch functions).


The following was written first, to serve as an illustration. I suspect it is much more helpful to the seeing (if at all), unfortunately. It includes the evaluation process step by step.

I will first re-write your cons function, as the equivalent and just a bit more verbose

(define cons 
   (lambda (x y)
     (letrec ([set-x! (lambda (v) (set! x v))]
              [set-y! (lambda (v) (set! y v))]
              [dispatch 
                  (lambda (m)
                    (cond ((eq? m 'car) x)
                          ((eq? m 'cdr) y)
                          ((eq? m 'set-car!) set-x!)
                          ((eq? m 'set-cdr!) set-y!)
                          (else (error 
                            "CONS: ERROR: unrecognized op name" m))))])
        dispatch)))

emphasizing more the value aspect of it, that lambda functions are values too, that they can be created, named, and returned. Now, the above means that writing (cons 1 2) in our code is the same as writing

(let ([x 1]
      [y 2])
     (letrec ; EXACTLY the same code as above
             ([set-x! (lambda (v) (set! x v))]
              [set-y! (lambda (v) (set! y v))]
              [dispatch 
                  (lambda (m)
                    (cond ((eq? m 'car) x)
                          ((eq? m 'cdr) y)
                          ((eq? m 'set-car!) set-x!)
                          ((eq? m 'set-cdr!) set-y!)
                          (else (error 
                            "CONS: ERROR: unrecognized op name" m))))])
        dispatch))

and when this is evaluated, two bindings are created – two places are set aside, one we can later on refer to as x, and the other as y – and each is filled with its corresponding value: for x it is the 1 that is put in there, and for y – 2. So far so good.

Then, the letrec form is entered. It creates its bindings, its three special places, named set-x!, set-y!, and dispatch. Each place is filled with its corresponding value, the corresponding lambda function that is created.

Here is the crucial part: since it is done inside that outer (let ([x 1] [y 2]) ...) form, each of these three functions knows about those two places, those two bindings, for x and y. Whenever x or y is used by set-x!, set-y!, or dispatch, what is actually referred to is the place for x, or for y, respectively.

Each of these three functions also knows about the other two, and about itself, being created by (letrec ...). That's how letrec works. With let, the names created by it only know about the enclosing environment.

And after the three functions are created, one of them, dispatch, is returned as the value of the whole thing, i.e. of the original call (cons 1 2).

We wrote (cons 1 2), and got back a value, a procedure dispatch that knows about the other two procedures, and also about the two value places, x and y.

This returned value, this procedure known as dispatch inside that letrec-created environment, we can call it with a message as an argument, that reads 'car, 'cdr, 'set-car!, or 'set-cdr!. And nothing else.

Stop. Go back a step. The "environment". The letrec-created "environment", created by that letrec form inside that let-created "environment". We can visualize this as two nested boxes. Two nested rectangles, the outer one created by let, with two places (two compartments, or "cells") set aside in it; and the inner one, created by letrec, with three compartments, three cells in it. Each box corresponding to its code fragment, code form like (let ...) or (letrec ...), that creates "bindings", or associations of names and places.

And actually, each such "box" is known as an environment frame; and all the nested boxes, each with its cells, together are known as the environment.

Each defined function has access to its box – that box in which it was created – and that function also has access to all the outer boxes in which its creation box happens to be enclosed. Just like the code forms are situated one inside the other. Which is exactly what "scope" means – a region of code where a name is known that refers to a place which holds a value.

Box inside a box inside a box... With compartments in them. Nothing more than that to it, really.

       ________________________________
      |                                |
      | (let   ([ .... ]               |
      |         [ .... ])              |
      |    ______________________      |
      |   | (letrec              |     |
      |   |      ([ .... ]       |     |
      |   |       [ .... ]       |     |
      |   |       [ .... ])      |     |
      |   |   ......             |     |
      |   |   ......             |     |
      |   |   )                  |     |
      |   *----------------------*     |
      |   )                            |
      *--------------------------------*

And when the dispatch value is returned, which is a procedure stored under that name in that inner environment frame, it also has a hidden pointer to that inner frame created by (letrec ...). And that frame also has a hidden pointer to its enclosing box's environment frame, created by the (let ...) form.

When the let box (code region, i.e. scope) is entered, its frame is created. When the letrec box (scope) is entered, its frame is created. The outer box's frame knows nothing about the enclosed box's frame, being created before it. The innermost box's frame has access to all the frames of all the boxes around it, starting with the one immediately around it. So this goes in a kind of inside-out manner: the inner box's frame contains a pointer to the outer box's frame whereas the outer box (code region, or scope) contains the inner box (code region).

So when we call (((cons 1 2) 'set-car!) 17), it is progressively interpreted as

(((cons 1 2) 'set-car!) 17)
=>
(( {dispatch where {E2: set-x!=..., set-y!=..., dispatch=... 
                    where {E1: x=1, y=2 }}} 'set-car!) 17)
=>
(  {(dispatch 'set-car!)
             where {E2: set-x!=..., set-y!=..., dispatch=... 
                    where {E1: x=1, y=2 }}} 17)
=>
(  {set-x!   where {E2: set-x!=..., set-y!=..., dispatch=... 
                    where {E1: x=1, y=2 }}} 17)
=>
{(set-x! 17) where {E2: set-x!=..., set-y!=..., dispatch=... 
                    where {E1: x=1, y=2 }}}
=>
{(set! x 17) where {E2: set-x!=..., set-y!=..., dispatch=... 
                    where {E1: x=1, y=2 }}}
=>
{(set! x 17) where {E1: x=1, y=2 }}
=>
{E1: x=17, y=2 }
     

Because set! actually changes the value stored in the cell, this change will be visible from now on in the rest of the program:

(define v (cons 1 2))
=>
{dispatch where {E2: set-x!=..., set-y!=..., dispatch=... where {E1: x=1, y=2 }}}
;
((v 'set-car!) 17)
=>
{dispatch where {E2: set-x!=..., set-y!=..., dispatch=... where {E1: x=17, y=2 }}}
;
(v 'car)
=>
({dispatch where {E2: set-x!=..., set-y!=..., dispatch=... where {E1: x=17, y=2 }}} 'car)
=>
{ (dispatch 'car) where {E2: set-x!=..., set-y!=..., dispatch=... where {E1: x=17, y=2 }}}
=>
{ x where {E2: set-x!=..., set-y!=..., dispatch=... where {E1: x=17, y=2 }}}
=>
{ x where {E1: x=17, y=2 }}
=>
17

Hopefully this pseudocode is clear enough. Next,

(define v (cons 1 2))
=>
{dispatch where {E2: set-x!=..., set-y!=..., dispatch=...
          where {E1: x=1, y=2 }}}
;
(define z (cons v v))
=>
{dispatch where {E5: set-x!=..., set-y!=..., dispatch=...
          where {E4: x=v, y=v
          where {E3: v={dispatch where {E2: set-x!=..., set-y!=..., dispatch=...
                                 where {E1: x=1, y=2 }}} }}}}

Here we chose the top-level evaluation strategy so that each new top-level command's environment frame is enclosed in the preceding one's.

(((z 'cdr) 'set-car!) 17)
=>
...... (z 'cdr)
...... =>
...... {(dispatch 'cdr) where {E5: set-x!=..., set-y!=..., dispatch=...
......     where {E4: x=v, y=v
......     where {E3: v={dispatch where {E2: set-x!=..., set-y!=..., dispatch=...
......                            where {E1: x=1, y=2 }}} }}}}
...... =>
...... { x where {E5: set-x!=..., set-y!=..., dispatch=...
......     where {E4: x=v, y=v

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...