EnvDraw is a cool program that you can run on your class account to help you draw environment diagrams. To use it:
envdraw
into a terminal. (If this doesn't work, first SSH into torus, and then type the command.) This command should then open an STk interpreter.(envdraw)
.EnvDraw>
prompt, try defining the square
function. Then, look at the EnvDraw window and see what happens!Now you're going to start drawing your own environment diagrams! We'll start from the basics and gradually build on them. Literally all of the rules you need to draw these diagrams are in the previous section. Make sure you know all of them by the time you're done with this lesson.
All of the example environment diagrams we show you in this section are taken from the EnvDraw program.
Let's start by defining a variable in Scheme and looking at the corresponding environment diagram. Specifically, we'll try drawing the diagram for:
(define x 3)
The first step is to always draw the global environment. Make sure to label
it! Next, we need to figure out how to handle the define
. By looking at the
rules in the previous section, you should learn that define
adds a new
binding to the current frame. Let's draw it out:
That's all there is to it! Simply write "x" with an arrow pointing to 3, all within the global environment. (Note: you don't have to write "[other bindings]" in your own environment diagrams for this class. EnvDraw does this for completeness.)
Now we're going to move onto defining procedures. What happens when you type the following code into a STk interpreter? We'll continue off of our diagram from earlier.
(define (square x) (* x x))
The first thing we're going to do is change the above code so it uses a lambda:
(define square (lambda (x) (* x x)))
Notice that this expression now has all of the same basic parts as (define x
3)
. So we follow the exact same procedure: write "square" in the global frame
and draw an arrow pointing to the lambda. We draw a lambda as a double-bubble.
The first bubble points to the arguments and the body. The second bubble
points to the defining environment, or the current environment at the time the
lambda is seen. This is how your diagram should now look.
Now you're done! To recap, you first need to draw the lambda. Make the first bubble point to the args and body, and the second point to the defining environment. Next, simply write "square" in the global environment and make it point to the lambda.
In the next section, we'll cover how to actually call the square
function we
just defined.
There is one very important point we're ignoring in these examples: define
doesn't always add things to the global environment. Instead, it adds it to
the current frame (which happens to be the global environment in the cases
above). We'll walk you through how to figure out what the current frame is in
a later section.
Now, let's draw the environment diagram for:
(define y (+ 3 4))
The difference from the previous example is that we must first apply the +
procedure to 3
and 4
before we can assign the value of y
. You can assume that all primitive procedures are applied by magic. Nothing needs to be drawn out for them. Thus, the full environment diagram would simply look like:
Let's say we now want to actually call the square
function we defined earlier.
We'll call it with the code:
(square 5)
To call a user-defined procedure, we follow the following steps:
The last step doesn't actually involve changing the environment diagram.
Instead, this is when we finally find the value of the call. To evaluate the
body of square, we must first figure out the value of x
. We always use the
first available binding for a variable. This means we look at the binding of
x -> 5
in our current frame, rather than the binding of x -> 3
in the global
frame. Once we've figured out the value of x
, we multiply it to itself
(remember, you can just assume this is done by magic). And now we're done!
We've multiplied 5
by itself, yielding the answer of 25
.
Remember, only compound procedure invocation creates a new frame!
The trick to evaluating atomic expressions (such as finding the value of a symbol) relies on figuring out which frame is the current frame. Before we get into that, remember that the rules of evaluating atomic expressions are:
All of the hard work is in case 2 above. Recall our environment diagram from the last section:
Remember that a new frame is only drawn when invoking a user-defined procedure. Thus, the current frame can only differ from the global environment when you are within the scope of another function. While this is a super important point, don't sweat it too much for now. Make sure you understand all of the examples thus far. We'll introduce more complicated examples further in the lesson.
x
into the interpreter, what will its value be?
cube
as follows:
(define (cube x) (* y y y))
First, draw the environment diagram corresponding to this definition. Add this definition on to the environment diagram we've drawn so far.
(cube 2)
What does it output?
At this point, our environment diagram now has three frames, the global frame, E1, and E2. E1 and E2 were created by calls to square
and cube
, respectively. However, once these functions return (or finish), the frames E1 and E2 we've created are useless! They are no longer reachable and their bindings no longer matter.
This isn't always the case. In the next sections we'll go over some code that makes these frames useful much after the initial procedure call.
set!
Now let's look at how to handle set!
. You may recall that set!
changes the
first available binding. Remember that we find the first available binding
by looking in the current frame, and then looking in the frames "behind" that
frame.
(define x 3)
(define (change x n)
(set! x n))
(change x 5)
x
?
change
procedure so the value of x
in the global environment changes? Indicate ALL possible fixes.
let
Using let
tends to cause a lot of students trouble. But don't despair! Whenever you're having troubles with let
, remember these simple rules:
let
into a lambda statement plus invocation.For example, you can rewrite
(let ((x 7)
(y 10))
(+ x y))
as
((lambda (x y) (+ x y)) 7 10)
lambda
. Remember, a lambda
is just a double-bubble with the correct arrows.lambda
with the proper arguments. Remember, this includes drawing a new frame and binding the formal parameters to the actual argument values.If you can remember these simple rules, you'll have no trouble at all!
(let ((x 7)
( y 10))
(+ x y))
(define (make-withdraw initial-amount)
(let ((balance initial-amount))
(lambda (amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient funds"))))
(define W1 (make-withdraw 100))
(W1 50)
At this point, you know everything you need to draw any environment diagram, no matter how complicated! As we go through problems in the future, don't forget the basics! Even the most complicated pieces of code can be boiled down into simple rules.
If there is even a single thing covered so far that you don't understand, ask for help! Environment diagrams is one of those topics that a lot of students find difficult.
Using our new-found knowledge of the environment model of evaluation, in the next section we're going to walk through how to implement OOP by clever-ly using lambdas and lets.
When you are taking Quiz 8, you are allowed to have a copy of the Environment Diagram Rules in addition to your double-sided cheat-sheet.