local expressions and scoping
(local [definition ...] expression)
local allows one or more
definitions to be defined for usage within an
expression body of a
local, there are two things to note:
expressionhas full access to variables or functions defined in outer scopes (think of scoping as a one-way trickle-down, where inner scopes can access identifiers bound in outer scopes, but not vice versa).
- This is arguably the only exception to the above, but when there is a naming conflict between a variable or function defined at the upper level and one defined in
[definitions ...], the
locally defined name takes precedence (this is called shadowing).
(define my-variable 10) (define (global-function n) (* n 2)) (define (another-function x) (local [; Note redefinition of `my-variable` from outer scope! (define my-variable 200)] ; This expression shadows the locally defined `my-variable` (+ my-variable ; Uses `global-function` defined at the global scope (global-function x)))) > (another-function 3) 206 ; 200 + (3 * 2) ; Outside the local scope, `my-variable` takes on the globally-defined ; value once again. > (+ my-variable 5) 15 ; would be 205 using shadowed value of `my-variable`
It may be helpful to articulate variable shadowing in terms of "overwriting" globally-bound names within the body of the
local expression. However, if you do use this analogy, take care to emphasize that the "overwriting" only occurs within the body of the
local; expressions outside the scope created by
local will not be able to reference locally-bound values.
(define (yet-another-function x) (local [(define y 10)] (+ x y))) > (yet-another-function 5) 15 > y y: this variable is not defined
In EECS 111,
local is most frequently used to provide a wrapper around functions with accumulators.
Suppose we have the following implementation of a function to sum up a list:
(define (sum-with-accumulator lst partial-sum) (cond [(empty? lst) partial-sum] [else (sum-with-accumulator (rest lst) (+ partial-sum (first lst)))]))
It wisely uses an accumulator pattern to remain space-efficient. However, the caveat of using accumulators is that we need to initalize them to some value (just like with
foldr). So in practice, using
sum-with-accumulator would look like this:
(sum-with-accumulator '(1 2 3) 0)
This is pretty suboptimal for a couple reasons:
- It's not user-friendly to have to keep adding the
0every time. Since the
0should never change (i.e. the function is designed to work with an initial accumulator of
0), it can easily be hard-coded.
- The entire concept of an accumulator is a question of implementation, which should be abstracted from the function API. The user doesn't need to know how the
sumoperation is constructed. Moreover, exposing the initial accumulator to the user only introduces the possibility of something going wrong (what if someone decides to use a different value as the initial accumulator?).
So we refactor, using a
local expression to encapsulate the recursive function and initial accumulator from the user:
(define (my-sum lst) (local [(define (sum-with-accumulator lst partial-sum) (cond [(empty? lst) partial-sum] [else (sum-with-accumulator (rest lst) (+ partial-sum (first lst)))]))] (sum-with-accumulator lst 0)))
Now we can invoke our function
my-sum like so:
(my-sum '(1 2 3)) ; avoids the need to specify 0