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