If you have not already done so, please take the quiz before continuing with lab.
Instructions for students & labbies: Students use DrScheme, following the design recipe, on the exercises at their own pace, while labbies wander among the students, answering questions, bringing the more important ones to the lab's attention.
Quick Review of Scope
A function definition introduces a new local scope -- the parameters are defined within the body.
(define (parameters …)
body)
A local definition
introduces a new local scope -- the names in the definitions are defined
within both within the bodies of the definitions and within the
local's body.
(local [definitions …]
body)
Note that the use of square brackets [ ] here is equivalent
to parentheses ( ). The brackets help set off the definitions
a little more clearly for readability.
In order to use local and the other features about
to be introduced in class, you need to set the DrScheme language level to
"Intermediate".
Exercises
Let's consider the problem of finding the maximum number in a list.
In general, there's an issue answering "What is the maximum number in
an empty list?"
To simplify the problem for today's goal, we'll only use non-negative
numbers, and define the maximum number of the empty list to be zero.
(Alternatively, we could write the function
only for non-empty lists.)
|
In general, you can take any program using local,
and turn it into an equivalent program without local.
Using local doesn't let us write programs which were impossible
before, but it does let us write them better.
We'll return to an example seen in a
previous lab.
We'll develop functions returning the list of positive numbers
1…n (left-to-right), given n as input.
|
Advice on when to use local
These examples point out the reasons why to use local:
- Avoid code duplication. I.e., increase code reuse.
- Avoid recomputation. This sometimes provides dramatic benefits.
- Avoid re-mentioning an unchanging argument.
-
To hide the name of helper functions.
An Aside While important conceptually, most programming languages (including Scheme) provide alternate mechanisms for this which scale better to large programs. These mechanisms typically have names like modules or packages. As this one example shows, even small programs can get a bit less clear when hiding lots of helpers. - Name individual parts of a computation, for clarity.
On the other hand, don't get carried away.
Here are two easy ways to misuse local:
-
; max-of-list: non-empty-list -> number (define (max-of-list a-nelon) (local [(define max-rest (max-of-list (rest a-nelon)))] (cond [(empty? (rest a-nelon)) (first a-nelon)] [else (cond [(< (first a-nelon) max-rest) max-rest] [else (first a-nelon)])])))
Make sure you don't put aQuestion What's wrong with this? localtoo early in your code. -
; max-of-list: non-empty-list -> number (define (max-of-list a-nelon) (cond [(empty? (rest a-nelon)) (first a-nelon)] [else (local [(define max-rest (max-of-list (rest a-nelon))) (define first-smaller? (< (first a-nelon) max-rest))] (cond [first-smaller? max-rest] [else (first a-nelon)]))]))This isn't wrong, but the local definition offirst-smaller?is unnecessary. Since the comparison is only used once, this is not a case of code reuse or recomputation. It provides a name to a part of the computation, but whether that improves clarity in this case is a judgement call.
Scope and the semantics of local
How does a local evaluate? The following is one way to
understand it.
-
For each
defined name in the list of definitions, rename it something entirely unique, consistently through the entirelocal. -
Lift those defines to the top level, and erase the surrounding
local, leaving only the body-expression.
Example: Observe how the rewriting makes it clear that there are really two independent placeholders.
(define x 5)
(local [(define x 7)]
x)
⇒
(define x 5)
(local [(define x^ 7)]
x^)
⇒
(define x 5)
(define x^ 7)
x^
Example:
(define x 5)
(define y x)
(define z (+ x y))
(local [(define x 7)
(define y x)]
(+ x y z))
⇒
(define x 5)
(define y x)
(define z (+ x y))
(define x^ 7)
(define y^ x^)
(+ x^ y^ z)
As an equivalent way of looking at things, we can look at the original code and ask which definition corresponds to each placeholder use. The answer is simple -- it is always the closest (in terms of nestedness) enclosing binding definition. We have three forms of binding:
- Parameters of a function.
- Local definitions.
-
"Global" definitions. (This is a special case of local definitions,
if we simply consider there to be an implicit
localaround everything.)
The actual way local is implemented is along these lines.
The interpreter keeps track of these bindings in an
environment, which maps placeholder names to their values.
Then, an expression is evaluated in the context of the current
environment.
|
DrScheme provides an easy way to look at this: the Check Syntax button. Clicking on this does two things. First, it checks for syntactic correctness of your program in the definitions window. If there are errors, it reports the first one. But, if there aren't any, it then annotates your code with various colors and, when you move your cursor on top of a placeholder, draws arrows between placeholder definitions and uses.
grow "knows" which placeholder named
growth-rate was in scope when it was defined, because that
knowledge is part of grow's closure.
|
In each of the following, determine which definition corresponds to
each placeholder usage.
As a result, first figure out without DrScheme
what each example produces.
To do so, you may want to use local's hand-evaluation
rules above.
Then confirm your results by using DrScheme.
(Note: Some examples give errors about unbound placeholders.)
|