This handout alone will not teach you how to draw environment model diagrams. However, this should be a helpful reference after you have seen examples in lecture and recitation.
id, id1, id2, id3, ... e, e1, e2, e3, ... v, v1, v2, v3, ...to represent identifiers, expressions, and values.
max=42
and
x=false
inside. Every frame has a "parent
pointer" which is drawn as an arrow from the frame's top
pointing to some other environment.
An "environment" is linked list of frames ending with the
global environment. (We sometimes blur the distinction between
frames and environments, because a frame is always the start of an
environment.)
When we say something like "extend an environment" we mean
that we are creating a new frame which has that environment as its
parent.
x: true
). Boxed values are drawn as an arrow
starting in the frame and pointing to wherever (in the diagram) the
value is actually stored.
fn x => e
goes in the left
piece, and the arrow representing the environment pointer starts in
the right piece. Another way is as two circles (like the number
"8"), with the arguments listed beside the upper circle,
the body beside the lower circle, and the environment pointer
starting inside the lower circle.
Every environment model diagram starts out with the gobal environment.
In addition, there is a "current environment" (which initially points to the global environment) and an "environment stack" which we use to keep track of environments, which is initially empty. (Often we use terminology like "save the current environment" to mean "push" and "return to the previous environment" to mean "pop".) The current environment is usually indicated using your finger, and the environment stack is usually not shown, but feel free to actually use paper if you have trouble keeping track of the environments.
This is the core of the environment model. Initially we are evaluating some (large) expression in the global environment, but recursively we will end up evaluating some of the subparts in different environments.
Simple expressions (those with no rule below) are evaluated using substitution model rules (which generally just means returning the result). Uses of builtin and library functions generally also fall into this category.
To evaluate a tuple expression (e1, e2, ..., en)
, first
evaluate e1 in the current environment to get a value (call it v1), then
evaluate e2 to get v2, etc. Then draw a horizontal box divided into n
pieces; put each v in the appropriate spot (if v is an unboxed value,
draw it inside the box; if it is a boxed value draw an arrow starting in
the box). Finally, return an arrow pointing to the tuple.
(Hint: evaluating an expression like the above is the
only thing that causes a new tuple box to be drawn.)
Record expressions are evaluated just like tuple expressions, except we have to label the fields. (We hardly ever use records in environment model examples, so you don't need to memorize the exact way we draw them. Just know that they're like tuples with labels on the fields.)
Recall that SML's builtin lists are just tuples:
datatype 'a list = nil | :: of ('a * 'a list)Therefore we draw lists as 2-tuples (called "cons cells") with the first item being some value (using the rules for boxed/unboxed values from the tuple section above) and the second item being either nil (sometimes drawn as an X in the box) or an arrow pointing to the rest of the list. (Hint: new cons cells only appear when using the
::
operator, the [e1, e2, ...]
special syntax, or library functions like map
.)
To find the value of a variable, look it up in the current environment. That means look for a binding of that identifier to a value in the current frame. If none is found, look in its parent frame, and then its grandparent, etc. If you make it all the way to the global environment and there's still no binding, then an error should occur. (Of course, the SML typechecker prevents this sort of error from occurring at runtime, but when drawing environment model diagrams we often will just check for type errors as we go.)
For the time being, we will only consider val declarations in our let expressions. To evaluate an expression like:
let val id1 = e1 in e end
To evaluate a let expression with multiple declarations, intuitively, consider sequential "nested lets" (where the outermost let has the first declarations, and all of the others occur within the body of the outermost let). This can be seen by example:
let val id1 = e1 val id2 = e2 val id3 = e3 in e endTreat it just like:
let val id1 = e1 in let val id2 = e2 in let val id3 = e3 in e end end endIn other words, to evaluate an expression like:
let val id1 = e1 ... val idn = en in e end
To evaluate an expression like:
fn (id1, id2, ...) => eSimply create a closure with the specified parameters and body. The environment pointer of the closure points to the current environment. (Hint: be careful when thinking about what the "current environment" is. For example, see the hint for let expressions above, and think about how this affects anonymous functions in declarations.)
To evaluate an expression like:
e1(e2)
Now we return to let expressions to add recursive functions. To evaluate an expression like:
let fun id(id1, id2, ...) = e1 in e end
fn (id1, id2, ...) => e1
in this new
frame to get a closure. The closure, according to the rules in
the closure section above, has parameters id1, id2, etc., body
e1, and environment pointer pointing to this new frame.
(Hint: that environment pointer is the key to
recursive functions.)
Let expressions with multiple fun and val declarations are handled in the way we described earlier for multiple val declarations; simply do them sequentially, getting one new frame for each declaration, using the details from the val or fun section as appropriate.
We don't often use mutually recursive functions in environment model examples, but when evaluating something like:
let fun f(x) = e1 and g(y) = e2 in e endthe idea is to make a new empty frame, create two closures (each with environment pointer pointing to the new frame) and then add bindings for both f and g to that new frame. Thus both e1 and e2 have both f and g in scope.
To evaluate an expression like:
ref e
ref
function is
applied to an expression.)
To evaluate an expression like:
!e
To evaluate an expression like:
e1 := e2
You should be able to extrapolate how arrays work from this (remember that refs are essentially one-element arrays), although it is unlikely we will ever do an array environment model example.
Recall that in SML, functions only take one argument (which might be a tuple). You may notice that when we do environment model diagrams we sometimes ignore this and act as if functions have multiple arguments. Similarly, we didn't introduce a formalism for when pattern matching is done in a let expression. In both cases, we simply make a frame with several bindings in it.
We also won't use case expressions often in the environment model, but should one come up, the idea is to first evaluate the expression at the top in the current environment, then find the case that matches and extend the current environment with a frame containing the bindings for whatever variables the pattern binds.