local
First, to use local, change DrScheme's language level to "Intermediate Student".
Index:
What is local
?,
Scope,
When to use local
?,
When not to use local
?,
local
& the Design Methodology
Note to labbies: This lab includes a lot of text, much of it review. Rather than echoing it all, emphasize the examples and exercises. Plan ahead on how to deliver the examples without writing everything out.
local
?
Let's start with a quick review of local
, as introduced
in class. Its syntax is
(local [definitions] expression)for example,
(local [(define x 3) (define y (+ x 1))] (+ x y))
How do we evaluate it? Basically, we
local
's expression body.
x
to
x'
, so that the resulting names have not been used before
and will not be used again. So, we amend the previous evaluation strategy
to
local
's expression body,
local
's expression body.
local
does not allow you to solve any problems that you couldn't
solve before, but it does allow you to solve them with better
programs.
Hand-evaluate all steps of the following program: (define x 3) (define z 6) (local [(define x 7) (define y (+ x 4))] (+ x y z)) |
For the curious...
You can also use |
Previously, in programs like
(define x 3) (define (cube x) (* x x x)) (cube 4)we knew that the variable x inside
cube
was
somehow different from the one outside. We didn't discuss this much,
since it was fairly intuitive. Our terminology is that the
x inside the function is local,
while the other is global. We also say that the local
variable shadows (or hides or masks)
the identically-named global variable.
If you use DrScheme's "Check Syntax" button, it will show you which use corresponds to which definition.
Use DrScheme's "Check Syntax" with the cube example,
and look at the various arrows.
|
This distinction is one of scope. I.e., we can have
multiple distinct variables with the same name. Each variable has a
scope -- the part of the code where that variable can be referred to.
Previously, we variables either had local scope or global scope. Now,
local
allows us to nest definitons and make further
distinctions. I.e., some variables will be "more local" than others.
Thinking about scope is basically a shortcut to the whole renaming step of
local
evaluation. Once you understand scope, you simply say that
a use of variable x refers to the "most local" definition
of x.
The scoping rules are rather simple:
Global definitions: The scope of x in a top-level definition
(define x expression) (define (x ...) expression)is the entire program. It is not just the remainder of the program following this definition, since that would disallow mutual recursion. (The variable x does not have a value until its
define
statement
has been executed, but the variable still exists and can be
referenced inside
a function definition that precedes the defintion of
x.)
Function parameters: The scope of x in
(define (f ... x ...) expression)is the function body expression.
Local definitions:
The scope of a define
'd variable
in a local
's definitions is both all of the
definitions in the enclosing local
(including those
that precede it!) and the local
's expression body.
For each of the following,
; For use in each example: (define x 1) (define y 2) (define z 3) ; Example 1: (define (fee x y) (local [(define z 7) (define y 4)] (+ x y z))) (fee x z) ; Example 2: (define (fie x y) (local [(define z 7) (define y (+ y 3))] (+ x y z))) (fie x z) ; Example 3: (define (foe1 x y) (local [(define z 7) (define y (+ x z)) (define x (local [(define y 10)] (+ y z)))] (+ x y z))) (foe1 x z) ; Example 4: (define (foe2 x y) (local [(define z 7) (define x (local [(define y 10)] (+ y z))) (define y (+ x z))] (+ x y z))) (foe2 x z) ; Example 5: (define (fum x y) (local [(define fum 7) (define (x z) (+ y z))] (x fum))) (fum x z) ; Example 6: (define (foo x y) (local [(define (z y) (cond [(zero? y) 1] [(positive? y) (* x (z (sub1 y)))]))] (local [(define x (+ y 1))] (+ x (z y))))) (foo x z)
Fortunately, real-world examples as convoluted as these are
uncommon. However, the last example's use of x in
|
local
?
There are several overlapping reasons for using local
,
as described in class:
Reuse code and avoid duplicating code.
From class, we saw that the original version of computing the maximum element of a list was inefficient because is recomputed recursive calls. By only making the recursive call once, we improved the algorithm to the following:
; max-score : list-of-num -> num ; Returns the largest of a list of numbers. ; Assumes all numbers are non-negative, so that the maximum of no ; numbers is zero. (define (max-score a-lon) (cond [(empty? a-lon) 0] ; the minimum score [(cons? a-lon) (local [(define max-score-rest (max-score (rest a-lon)))] (cond [(< (first a-lon) max-score-rest) max-score-rest] [else (first a-lon)]))]))
Avoiding duplication also helps stylistically, since it is a special case of our general rule to reuse code when possible. It also helps avoid errors.
Eliding invariant function arguments. A variant of our in-class example:
; is-in-los? : list-of-symbol symbol -> boolean ; Returns whether the symbol is in the list. (define (is-in-los? a-los a-sym) (local [ ; is-in-alos? : list-of-symbol -> boolean ; Returns whether the symbol is in the list. (define (is-in-alos? a-los) (cond [(empty? a-los) false] [(cons? a-los) (or (symbol=? (first a-los) a-sym) (is-in-alos? (rest a-los)))]))] (is-in-alos? a-los)))The argument a-sym is invariant in the search, and this technique allows us to not pass it around everywhere.
For the curious... At the lowest levels of implementation, this idea can improve efficiency, but whether it does at the high-level of Scheme is unclear, because Scheme has lots of leeway in implementing your code. |
To hide a helper function or variable from other code.
Another way to compute the maximum of a list would introduce a helper function:
; max : num num -> num ; Returns the larger of two numbers. (define (max n1 n2) ...) ; max-score : list-of-nums -> num ; Returns the largest of a list of numbers. ; Assumes all numbers are non-negative, so that the maximum of no ; numbers is zero. (define (max-score a-lon) (cond [(empty? a-lon) 0] [(cons? a-long) (max (first a-lon) (max-score (rest a-lon)))]))We could make the helper function local, as in either
; max-score : list-of-nums -> num ; Returns the largest of a list of numbers. ; Assumes all numbers are non-negative, so that the maximum of no ; numbers is zero. (define (max-score a-lon) (local [ ; max : num num -> num ; Returns the larger of two numbers. (define (max n1 n2) ...) ] (cond [(empty? a-lon) 0] [(cons? a-long) (max (first a-lon) (max-score (rest a-lon)))])))or
; max-score : list-of-nums -> num ; Returns the largest of a list of numbers. ; Assumes all numbers are non-negative, so that the maximum of no ; numbers is zero. (define (max-score a-lon) (cond [(empty? a-lon) 0] [(cons? a-long) (local [ ; max : num num -> num ; Returns the larger of two numbers. (define (max n1 n2) ...) ] (max (first a-lon) (max-score (rest a-lon))))]))
This is a very important rule stylistically. On large projects with multiple programmers, for example, it is easy for multiple programmers to define helper functions with the same names. Hiding these prevents others from interfering with your programs' correctness.
Which of the above two options is better? Although each has its advantage, the first is generally the better option for helper functions.
In the end, your programs tend to look like
(define (my-main-function ...) (local [(define (helper1 ...) ...) (define (helper2 ...) ...) (define (helper3 ...) ...) ...] code-using-helpers))
To name interesting subcomputations.
Even when not accomplishing either above goal, it can be useful to name the results of subexpressions to improve the readability of your code.
As a small example, compare the readability of the two following definitions:
(define (rectangle-area upper-left-point lower-right-point) (* (abs (- (point-x upper-left-point) (point-x lower-right-point))) (abs (- (point-y upper-left-point) (point-y lower-right-point))))) (define (rectangle-area upper-left-point lower-right-point) (local [(define top-length (abs (- (point-x upper-left-point) (point-x lower-right-point)))) (define side-length (abs (- (point-y upper-left-point) (point-y lower-right-point))))] (* top-length side-length)))The latter makes is clearer what two quantities are being multiplied.
To do: Go back and review your last two assignments. Use
local
where appropriate.
You are expected to use local
on the current and future
assignments, where appropriate.
local
?Usually you shouldn't consider identical recursive calls in different conditional cases to be repeated code.
Consider the following code: ; count-symbols : list-of-numbers-and-symbols -> nat ; Returns the count of how many symbols appear in the list. (define (count-symbols a-lons) (cond [(empty? a-lons) 0] [(number? (first a-lons)) (count-symbols (rest a-lons))] [(symbol? (first a-lons)) (add1 (count-symbols (rest a-lons)))]))
In each of the following versions, what is wrong with the use of
; count-symbols : list-of-numbers-and-symbols -> nat ; Returns the count of how many symbols appear in the list. (define (count-symbols a-lons) (local [(define rest-count (count-symbols (rest a-lons)))] (cond [(empty? a-lons) 0] [(number? (first a-lons)) rest-count] [(symbol? (first a-lons)) (add1 rest-count)])) ; count-symbols : list-of-numbers-and-symbols -> nat ; Returns the count of how many symbols appear in the list. (define (count-symbols a-lons) (cond [(empty? a-lons) 0] [else (local [(define rest-count (lons-length (rest a-lons)))] (cond [(number? (first a-lons)) rest-count] [(symbol? (first a-lons)) (add1 rest-count)]))])) |
Answers. |
Don't bother naming uninteresting subcomputations.
local
& the Design MethodologyYou should still use the design methodology for developing local functions. In particular, local functions still need a contract and purpose.
However, there are complications. You can't test a local function
independently, because it is hidden. A standard technique is to
define the function globally for testing, then move it into a
local
. This technique doesn't work
when the function uses variables that aren't local to the function, as when
eliding invariant arguments, e.g.,
; expt : nat nat -> nat ; Returns x to the y power. (define (expt x y) (local [; expt-of-x : nat -> nat ; Returns x to the y power. (define (expt-of-x y) (cond [(zero? y) 1] [(positive? y) (* x (expt-of-x (sub1 y)))]))] (expt-of-x y)))