Module 05.1: Environments, Scope, and Closures¶
Module 04.2 ended with an interpreter that could represent functions and apply them using substitution.
That was a major step. It also left us with several problems.
Our evaluator could apply:
FunDef "x"
(BinOp (Var "x") PlusOp (Num 1))
to an argument by substituting the argument for x in the body. But substitution has an uncomfortable property: every function call may require us to walk through and rebuild a potentially large expression.
Even more importantly, we ended Module 04.2 with questions that substitution had not really settled:
- Which binding should a variable refer to when scopes are nested?
- What should happen when a function uses a variable that is not one of its parameters?
- If a function is returned as a value and called later, what information needs to travel with it?
This module develops a different model.
Instead of repeatedly rewriting expressions, we will keep track of bindings in an environment.
That one move leads directly to two of the most important ideas in programming languages:
scope determines which binding a variable refers to, and a closure packages a function together with the environment it needs.
Along the way, we will also add let expressions to our language and see a useful language-design technique: sometimes new syntax does not require fundamentally new semantics.
By the end of this module, you should be able to:
- represent a
letexpression in our abstract syntax, - explain how a
letexpression can be defined in terms of function definition and application, - explain why repeated substitution can create unnecessary work,
- describe an environment as a mapping from variable names to values,
- modify an evaluator so that variable lookup uses an environment,
- explain why the choice of environment data structure affects performance,
- use
letto create nested bindings and to give functions names, - distinguish dynamic scope from static (lexical) scope,
- explain why an environment-only evaluator loses information when functions escape their defining context,
- define a closure as a function together with its defining environment, and
- explain how closures give our interpreter static scope.
How to use this module
When you see a box labeled Gradescope question, answer that question in the Module 5.1 Completion assignment on Gradescope.
Other boxes labeled Pause and think, Try it, or Practice are there to help you understand the material. There is nothing to submit for those unless the box explicitly says Gradescope question.
Where We Are¶
Our language now has numbers, variables, binary operations, function definitions, and function applications:
type VarName = String
data Exp
= Var VarName
| Num Double
| BinOp Exp Op Exp
| FunDef VarName Exp
| Apply Exp Exp
deriving (Show, Eq)
A function definition contains a parameter name and a body:
FunDef "x"
(BinOp (Var "x") PlusOp (Num 1))
and a function application contains a function expression and an argument expression:
Apply
(FunDef "x"
(BinOp (Var "x") PlusOp (Num 1)))
(Num 5)
In Module 04.2, our first semantics for application was:
apply a function
=
substitute the argument for the parameter
+
evaluate the result
For example:
(x ↦ x + 1) 5
became:
x + 1
[x → 5]
and therefore:
5 + 1
which evaluates to 6.
That model gave us a concrete way to understand function application. Now we are going to ask whether physically rewriting the body is the best way to implement it.

Adding let Expressions¶
We have already used Haskell expressions like:
let x = 100
in x + 17
A let expression has three important pieces:
let x = 100 in x + 17
^ ^ ^
| | |
variable body
|
bound expression
It says:
- associate
xwith the expression100, - evaluate the body
x + 17with that binding available.
The result is:
117
Our little language does not have let yet.
So first we need syntax.
Gradescope question: Let expressions in the abstract syntax
Submit this response on Gradescope.
How can you add let-expressions to our abstract syntax?
Before continuing, sketch what information a Let constructor would need to store.
Representing a let¶
A direct representation is:
data Exp
= Var VarName
| Num Double
| BinOp Exp Op Exp
| Let VarName Exp Exp
| FunDef VarName Exp
| Apply Exp Exp
deriving (Show, Eq)
Read:
Let varname boundExp body
as:
let varname = boundExp in body
For example:
Let "x"
(Num 100)
(BinOp (Var "x") PlusOp (Num 17))
represents:
let x = 100
in x + 17
Notice how the datatype follows directly from the structure of the language feature.
A let needs:
- a variable name,
- an expression to bind to that variable,
- and the body in which the binding is available.
So far this gives us the abstract syntax of let.
We still need its semantics.
We Already Know How to Give let a Meaning¶
Think again about:
let x = 100
in x + 17
Informally, one way to describe its meaning is:
evaluate
x + 17with100in place ofx.
But we already built another language feature with almost exactly that behavior.
Consider the anonymous function:
x ↦ x + 17
applied to:
100
The result should be the same.
In our abstract syntax:
Let "x"
(Num 100)
(BinOp (Var "x") PlusOp (Num 17))
can be understood in terms of:
Apply
(FunDef "x"
(BinOp (Var "x") PlusOp (Num 17)))
(Num 100)
This gives us a useful implementation rule:
eval (Let varname boundExp body) =
eval (Apply (FunDef varname body) boundExp)
Gradescope question: Semantics of let
Submit this response on Gradescope.
How can you define the semantics of let-expressions using functions?
The important idea is larger than let.
New syntax can sometimes be defined using semantics we already have.
Instead of inventing an entirely new evaluation mechanism for Let, we translate it into function definition and application, whose meaning we already know.
This kind of convenience syntax is often called syntactic sugar: a new way to write something that could already be expressed using more primitive language features.
Here:
let x = e in body
is syntactic sugar for something like:
(x ↦ body) e
The new syntax can still be extremely useful. "Syntactic sugar" does not mean "unimportant." It means the feature does not require a fundamentally new semantic mechanism.

The Cost of Substitution¶
Our substitution semantics is conceptually simple:
apply function
-> substitute argument through body
-> evaluate
But consider a curried function with several parameters:
FunDef "a"
(FunDef "b"
(FunDef "c"
(FunDef "d"
(BinOp (Var "a") PlusOp
(BinOp (Var "b") PlusOp
(BinOp (Var "c") PlusOp (Var "d")))))))
and apply it to four arguments:
Apply
(Apply
(Apply
(Apply fourArgFunction (Num 1))
(Num 2))
(Num 3))
(Num 4)
After the first application, substitution has to traverse the remaining function body and replace occurrences of a.
After the second application, substitution traverses much of that structure again for b.
Then again for c.
Then again for d.
If the function body has size proportional to (N), and we perform (N) substitutions that each traverse an expression of roughly that size, we can end up doing work on the order of:
N substitutions
×
O(N) traversal per substitution
=
O(N²) work
The exact cost depends on the expression and implementation, but the pattern is the important part:
We keep walking through large pieces of syntax just to remember what values some names should stand for.
Can we avoid doing that?

A Different Idea: Remember the Bindings¶
Suppose we are evaluating:
Apply
(Apply
(Apply
(Apply fourArgFunction (Num 1))
(Num 2))
(Num 3))
(Num 4)
Instead of substituting:
a -> 1
through the whole remaining expression, we could simply record:
a = 1
Then, when we eventually encounter:
Var "a"
we look up its value.
After more applications, we might have accumulated:
a = 1
b = 2
c = 3
d = 4
Now evaluating:
BinOp (Var "a") PlusOp
(BinOp (Var "b") PlusOp
(BinOp (Var "c") PlusOp (Var "d")))
does not require us to rewrite the expression first.
We can leave the syntax alone and look up names only when we need them.
The data structure containing those bindings is called an:
environment
An environment is a mapping:
variable name -> value
For example:
"a" -> Num 1
"b" -> Num 2
"c" -> Num 3
"d" -> Num 4
This is the central move of the module:
substitution model:
change the expression to replace variables
environment model:
keep the expression
+ remember what the variables mean




Representing an Environment¶
For a first implementation, a list of pairs is enough:
type Env = [(VarName, Exp)]
For example:
[ ("a", Num 1)
, ("b", Num 2)
, ("c", Num 3)
]
We need two basic operations:
lookup: given a name, find its value
insert: add or update a binding
One straightforward implementation is:
envLookup :: VarName -> Env -> Exp
envLookup v [] =
error ("unbound identifier: " ++ v)
envLookup v ((key, val) : rest) =
if v == key
then val
else envLookup v rest
and:
envInsert :: VarName -> Exp -> Env -> Env
envInsert v val [] =
[(v, val)]
envInsert v val ((key, oldVal) : rest) =
if v == key
then (v, val) : rest
else (key, oldVal) : envInsert v val rest
This is not the only representation we could use. It is useful because the implementation makes the idea of an environment completely explicit.
Evaluating With an Environment¶
Our evaluator now needs another input.
Before:
eval :: Exp -> Exp
Now:
eval :: Env -> Exp -> Exp
The environment answers a question that the expression itself cannot answer:
If I encounter
Var "x", what does"x"currently mean?
Numbers¶
Numbers are already values:
eval env (Num x) =
Num x
The environment is irrelevant here.
Variables¶
A variable gets its meaning from the environment:
eval env (Var v) =
envLookup v env
This is the key replacement for substitution.
We do not rewrite every Var "v" in advance.
We leave it in the expression until evaluation actually reaches it, and then we look up v.
Binary operations¶
Recursive evaluation simply passes the same environment along:
eval env (BinOp left op right) =
evalOp (eval env left) op (eval env right)
Function definitions¶
For the moment, we will continue treating function definitions as values:
eval env (FunDef varname body) =
FunDef varname body
There is a problem hiding in this rule.
We will deliberately leave it there for a little while.
Function application¶
To apply a function:
- evaluate the function expression,
- evaluate the argument expression,
- associate the parameter name with the argument value,
- evaluate the function body using the extended environment.
eval env (Apply fexp arg) =
let FunDef varname body = eval env fexp
argval = eval env arg
newEnv = envInsert varname argval env
in eval newEnv body
This version is using call-by-value, because it evaluates arg before adding the result to the environment.
Compare this with substitution.
Old idea:
substitute the argument into every relevant place in the body
New idea:
remember parameter -> argument value
and look it up only when a variable is evaluated
Did We Actually Make It Faster?¶
We introduced environments partly because repeated substitution could require repeated traversals of large syntax trees.
But our first environment is:
type Env = [(VarName, Exp)]
How expensive is:
envLookup
in the worst case?
It may have to scan the entire list.
How expensive is our envInsert?
It may also have to scan the entire list.
So if an environment grows to size (N), each environment operation can itself require (O(N)) work.
If we perform (O(N)) such operations, we can still end up with (O(N^2)) work.
Gradescope question: Did the environment improve the running time?
Submit this response on Gradescope.
Did we achieve our goal of improving the running time with this implementation so far?
The idea of an environment lets us avoid repeatedly traversing and rebuilding the whole program expression.
But performance also depends on the data structure we choose to implement that environment.
Gradescope question: A better environment data structure
Submit this response on Gradescope.
Can you think of a better data structure to use for the environment, besides a list of pairs of variable and value?
A dictionary/map is the natural abstraction.
For example, Haskell's Data.Map provides logarithmic lookup and insertion, while hash-table-style maps can provide average constant-time lookup and insertion.
The exact implementation is not the central point here. The important design lesson is:
Once the semantics say "look up a name in an environment," we can choose an appropriate data structure to implement that operation efficiently.
If lookup and insertion were (O(1)), then an evaluator doing a constant amount of environment work at each of (N) syntax nodes could be (O(N)), rather than repeatedly traversing whole subtrees.
Representation and semantics are different questions
The semantics of an environment is just a mapping from names to values.
Whether we implement that mapping with a list, balanced tree, hash table, or something else is a separate implementation decision.

let Gets Better With Environments¶
Recall that we defined:
Let varname boundExp body
in terms of function application:
Apply (FunDef varname body) boundExp
So our evaluator can still use:
eval env (Let varname boundExp body) =
eval env (Apply (FunDef varname body) boundExp)
Now let naturally uses the environment machinery we just built.
For:
Let "x"
(Num 10)
(BinOp (Var "x") TimesOp (Num 2))
evaluation eventually extends the environment with:
"x" -> Num 10
and evaluates the body in that environment.
The expression itself does not have to be rewritten.
Nested lets and Shadowing¶
Environments also make nested bindings concrete.
Consider:
let y = 100 in
let x = 10 in
x + y
The corresponding abstract syntax is:
Let "y" (Num 100)
(Let "x" (Num 10)
(BinOp (Var "x") PlusOp (Var "y")))
While evaluating the inner body, the environment needs both bindings:
"y" -> Num 100
"x" -> Num 10
so the result is:
110
Now consider shadowing:
let x = 100 in
let x = 3 in
x
The occurrence of x in the innermost body should refer to the inner binding.
Conceptually, the environment visible there is the environment in which the newest relevant binding for x is 3.
When evaluation leaves that inner scope, the outer environment still exists unchanged. Haskell data is immutable, so extending an environment constructs a new environment rather than destructively modifying the old one.
This reconnects to Module 02.1
Earlier, we described nested Haskell scopes as resolving names from the innermost applicable scope outward.
We now have enough interpreter machinery to see how a language implementation can make that idea concrete.
let Can Give a Function a Name¶
Our FunDef values are anonymous:
FunDef "x"
(BinOp (Var "x") PlusOp (Num 1))
But now let gives us a way to bind such a value to a name:
let f = (\x -> x + 1) in
f 100
In our abstract syntax:
Let "f"
(FunDef "x"
(BinOp (Var "x") PlusOp (Num 1)))
(Apply (Var "f") (Num 100))
The environment can record:
"f" -> function value
Then:
Var "f"
is looked up just like any other variable.
The result should be:
Num 101
This is a useful consequence of something we emphasized earlier:
Functions are values.
An environment does not need a completely different lookup mechanism for numbers and functions. Both can be values associated with names.

Now Something Strange Happens¶
Consider this expression:
let v = 42 in
let f x = v + x in
f 100
There is only one visible binding for v, so we expect:
42 + 100 = 142
Now add another binding before the call:
let v = 42 in
let f x = v + x in
let v = 3 in
f 100
What should this evaluate to?
There are two plausible answers:
103
or:
142
Why?
The function's body is:
v + x
When we eventually call:
f 100
we know where x comes from:
x = 100
But where should the value of v come from?
At the moment of the call, the nearest active binding is:
v = 3
But at the moment f was defined, the visible binding was:
v = 42
Those two choices define two different notions of scope.
Gradescope question: Why do we get two different results?
Submit this response on Gradescope.
How might you explain the difference between the two different results? How is it related to the implementation of eval?

Dynamic Scope and Static Scope¶
The difference between 103 and 142 is not an arithmetic issue.
It is a language-design decision about scope.
Dynamic scope¶
Under dynamic scope, a free variable in a function body is resolved using bindings associated with the context in which the function is called.
For:
let v = 42 in
let f x = v + x in
let v = 3 in
f 100
when f 100 runs, the most recent applicable binding for v is:
v = 3
so:
v + x
=
3 + 100
=
103
Our first environment-based evaluator naturally drifts toward this behavior because function application evaluates the body using the environment available around the call:
eval env (Apply fexp arg) =
...
eval (envInsert varname argval env) body
^^^
caller's environment
That env is the crucial detail.
Static scope¶
Under static scope, also commonly called lexical scope, a free variable in a function body is resolved using the environment associated with where the function was defined.
When we create:
f x = v + x
the visible binding is:
v = 42
So that is the v the function should continue to use later, even if it is called inside another scope containing:
v = 3
The result becomes:
42 + 100 = 142
The distinction can be summarized as:
dynamic scope:
free variables use the calling environment
static / lexical scope:
free variables use the defining environment
Most modern general-purpose programming languages, including Haskell, use static/lexical scope.
Gradescope question: Which scope rule do you prefer?
Submit this response on Gradescope.
Do you prefer one version of the two options from the end of the video over the other? Why or why not?
There is no need to invent a universal philosophical answer. The useful exercise is to think about what each choice means for how a programmer reasons about a function.
With static scope, the meaning of:
f x = v + x
depends on the lexical context where that definition appears.
With dynamic scope, its meaning can change depending on the chain of function calls that leads to it.

A Deeper Problem: Returning a Function Loses Its Environment¶
There is another place where our environment-only evaluator breaks.
Recall how we represent a two-argument function using currying:
FunDef "a"
(FunDef "b"
(BinOp (Var "b") PlusOp (Var "a")))
Apply it to two arguments:
Apply
(Apply
(FunDef "a"
(FunDef "b"
(BinOp (Var "b") PlusOp (Var "a"))))
(Num 100))
(Num 42)
We expect:
100 + 42 = 142
Look at the first application:
Apply
(FunDef "a"
(FunDef "b"
(BinOp (Var "b") PlusOp (Var "a"))))
(Num 100)
Our evaluator temporarily creates an environment containing:
"a" -> Num 100
and evaluates the body:
FunDef "b"
(BinOp (Var "b") PlusOp (Var "a"))
But our rule for a function definition is currently:
eval env (FunDef varname body) =
FunDef varname body
It throws the environment away.
So the result of the first application is merely:
FunDef "b"
(BinOp (Var "b") PlusOp (Var "a"))
Where did:
"a" -> Num 100
go?
It disappeared.
When we later apply this returned function to 42, the evaluator eventually reaches:
Var "a"
and has no binding for a.
The result is an error such as:
unbound identifier: a
This reveals the real problem.
A function value is not always completely described by its parameter and body.
If its body refers to variables from an outer scope, then the function also needs the environment in which those variables received their meaning.
![A slide titled "Remember this?" showing eval [] (Apply (Apply (FuncDef](../img/05.1/curried-function-unbound-error.png)

Free Variables Are the Clue¶
Consider:
FunDef "x"
(BinOp (Var "x") PlusOp (Var "v"))
Inside the function body:
Var "x"
is associated with the function parameter.
But:
Var "v"
is not.
We call v a free variable of this function expression.
To understand the function later, we need to know what v meant when the function was created.
That suggests exactly what information a function value needs to carry:
parameter
+
body
+
environment from the point of definition
This package is called a:
closure
Closures¶
A closure combines:
- the function's parameter,
- the function's body,
- the environment that was active when the function was defined.
Conceptually:
closure
= function code
+ saved environment
We can represent that in our interpreter as another runtime form:
data Exp
= Var VarName
| Num Double
| BinOp Exp Op Exp
| Let VarName Exp Exp
| FunDef VarName Exp
| Apply Exp Exp
| Closure Env VarName Exp
deriving (Show, Eq)
Closure is a runtime value
We are storing Closure in the same Haskell datatype as our abstract syntax because it is convenient for this small interpreter.
Conceptually, though, a programmer does not write a Closure directly in the source language. A closure is a value the evaluator creates when it evaluates a function definition.
Now the function-definition rule changes.
Before:
eval env (FunDef varname body) =
FunDef varname body
After:
eval env (FunDef varname body) =
Closure env varname body
The environment is no longer discarded.
It becomes part of the function value.
Applying a Closure¶
Function application changes too.
The function expression should now evaluate to:
Closure closureEnv varname body
Then we:
- evaluate the argument in the current calling environment,
- extend the closure's saved environment with the parameter binding,
- evaluate the body in that environment.
eval env (Apply fexp arg) =
let Closure closureEnv varname body = eval env fexp
argval = eval env arg
bodyEnv = envInsert varname argval closureEnv
in eval bodyEnv body
Look carefully at the two environments in this rule.
The argument is evaluated using:
env
the environment of the call.
But the function body is evaluated using:
closureEnv
the environment saved when the function was defined.
That one distinction implements static scope.
definition time:
save environment in Closure
call time:
evaluate argument in caller's environment
evaluate body in saved defining environment

Revisit the Scope Example¶
Return to:
let v = 42 in
let f x = v + x in
let v = 3 in
f 100
When f is defined, the evaluator has an environment containing:
"v" -> Num 42
So evaluating the function definition produces conceptually:
Closure
saved environment: v = 42
parameter: x
body: v + x
Later we enter:
let v = 3 in ...
The calling environment now contains:
v = 3
But that does not replace the environment stored inside the closure.
When we call:
f 100
we extend the closure environment:
v = 42
x = 100
and evaluate:
v + x
there.
So we get:
142
The value 3 belongs to the calling context, but it does not change what the free variable v means inside f.
That is lexical scope made operational.
Revisit the Curried Function¶
Now reconsider:
FunDef "a"
(FunDef "b"
(BinOp (Var "b") PlusOp (Var "a")))
After applying the outer function to 100, evaluating the inner function definition no longer returns only:
FunDef "b"
(BinOp (Var "b") PlusOp (Var "a"))
Instead it produces a closure that remembers:
"a" -> Num 100
So when that returned function is later applied to 42, its body is evaluated in an environment containing:
"a" -> Num 100
"b" -> Num 42
and we finally get:
Num 142
This is why closures matter so much for higher-order functions.
A function can be:
- created in one scope,
- returned from that scope,
- stored somewhere,
- passed to another function,
- and called much later.
The lexical environment where it was created may no longer be the currently active environment.
The closure carries the necessary bindings along with the function.
Putting the Evaluator Together¶
Using our simple list environment, the core evaluator now looks like this:
type VarName = String
type Env = [(VarName, Exp)]
data Exp
= Var VarName
| Num Double
| BinOp Exp Op Exp
| Let VarName Exp Exp
| FunDef VarName Exp
| Apply Exp Exp
| Closure Env VarName Exp
deriving (Show, Eq)
envLookup :: VarName -> Env -> Exp
envLookup v [] =
error ("unbound identifier: " ++ v)
envLookup v ((key, val) : rest) =
if v == key
then val
else envLookup v rest
envInsert :: VarName -> Exp -> Env -> Env
envInsert v val [] =
[(v, val)]
envInsert v val ((key, oldVal) : rest) =
if v == key
then (v, val) : rest
else (key, oldVal) : envInsert v val rest
eval :: Env -> Exp -> Exp
eval env (Num x) =
Num x
eval env (Var v) =
envLookup v env
eval env (BinOp left op right) =
evalOp (eval env left) op (eval env right)
eval env (Let varname boundExp body) =
eval env (Apply (FunDef varname body) boundExp)
eval env (FunDef varname body) =
Closure env varname body
eval env (Apply fexp arg) =
let Closure closureEnv varname body = eval env fexp
argval = eval env arg
bodyEnv = envInsert varname argval closureEnv
in eval bodyEnv body
There are many directions in which a production interpreter would make this more robust. For now, focus on the semantic structure.
The environment handles variables¶
eval env (Var v) =
envLookup v env
Function definition captures the environment¶
eval env (FunDef varname body) =
Closure env varname body
Function application uses the captured environment¶
eval env (Apply fexp arg) =
...
eval bodyEnv body
where:
bodyEnv = envInsert varname argval closureEnv
Those three rules are the conceptual core of this module.
A Useful Way to Read the Whole Development¶
We started with substitution:
when applying a function:
rewrite the body
Then we introduced environments:
when applying a function:
record parameter -> value
when evaluating a variable:
look it up
That gave us an efficient and flexible way to separate:
the syntax of an expression
from:
the current meanings of its variable names
But a single "current" environment was not enough for functions.
So closures added one more rule:
when evaluating a function definition:
save the current environment with it
Now a function carries enough context to interpret its free variables later.
The progression is:
substitution
↓
environment
↓
scope question
↓
closure
Each step is motivated by a concrete problem in the previous one.
Check Your Understanding¶
Practice: Which environment?
Nothing to submit.
Consider:
let n = 10 in
let f x = n * x in
let n = 3 in
f 4
Under dynamic scope, which binding of n would the body of f use?
Under static scope, which binding would it use?
What result would each scope rule produce?
Reveal
Dynamic scope uses the calling context, where the nearest n is 3, so the result is 12.
Static scope uses the defining context, where n was 10, so the result is 40.
Practice: What belongs in the closure?
Nothing to submit.
Consider:
let offset = 7 in
let f x = x + offset in
f
If evaluating this expression returns the function f as a value, what information must that function value remember so it can still be called correctly later?
Reveal
It needs the function's parameter (x), its body (x + offset), and an environment that preserves the relevant binding offset = 7.
What to Carry Forward¶
The central ideas are now connected.
let can be understood using functions¶
let x = e in body
can be interpreted as:
apply (x ↦ body) to e
This is an example of defining new syntax in terms of existing semantics.
An environment records bindings¶
Instead of rewriting a whole expression every time a function is applied, we can remember:
name -> value
and look up names when evaluation reaches them.
Representation affects performance¶
A list of pairs is easy to understand, but its lookup and insertion can be linear in the size of the environment.
The semantic notion of an environment does not force us to use that representation. We can choose a more appropriate mapping structure.
Scope decides which binding a variable means¶
Dynamic scope uses bindings from the calling context.
Static or lexical scope uses bindings from the context where the function was defined.
A function value needs context¶
A function body can contain free variables.
If that function can outlive the scope where it was created, it must carry the bindings those variables need.
That is what a closure does:
closure
=
function
+
defining environment
This is the mechanism that lets higher-order functions and lexical scope work together.
Where This Leaves Us¶
At the end of Module 04.2, function application meant:
substitute
then evaluate
Now our evaluator has a richer model:
variables get meaning from environments
functions capture environments
applications extend captured environments
The interpreter is no longer just manipulating syntax trees. It is explicitly representing the context in which expressions have meaning.
That is a major step toward a realistic language implementation.
It also gives concrete machinery for ideas that appeared much earlier in the course:
bindings
+
nested scopes
+
functions as values
+
higher-order functions
+
code as data
=
environments and closures
The next time you encounter a function that uses values from outside its parameter list, especially a function that is returned or passed somewhere else, you should now have a precise question to ask:
What environment does this function carry with it?
Finish the Module 05.1 Completion¶
Make sure you have submitted responses to the six substantive Gradescope questions that appeared at the relevant points above:
- How can you add let-expressions to our abstract syntax?
- How can you define the semantics of let-expressions using functions?
- Did we achieve our goal of improving the running time with this implementation so far?
- Can you think of a better data structure to use for the environment, besides a list of pairs of variable and value?
- How might you explain the difference between the two different results? How is it related to the implementation of eval?
- Do you prefer one version of the two options from the end of the video over the other? Why or why not?
Gradescope also asks:
Gradescope question: Time spent
Submit this response on Gradescope.
Approximately how much time did you spend on this module?
- less than 30 minutes
- 30 - 45 minutes
- 45 - 60 minutes
- 60 - 75 minutes
- 75 - 90 minutes
- more than 90 minutes
Gradescope question: Lingering questions
Submit this response on Gradescope.
What lingering questions or thoughts do you have about this module?