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)])])))Question What's wrong with this? Make sure you don't put a
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 of
first-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.
In each case, |
|
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 (Note - Some examples give errors about unbound placeholders.)
|
This work is licensed under a Creative Commons Attribution 2.5 License. Please follow our