The following Scheme code
(let ((x 1))
(define (f y) (+ x y))
(set! x 2)
(f 3) )
which evaluates to 5 instead of 4. It is surprising considering Scheme promotes static scoping. Allowing subsequent mutation to affect bindings in the closed environment in a closure seems to revert to kinda dynamic scoping. Any specific reason that it is allowed?
EDIT:
I realized the code above is less obvious to reveal the problem I am concerned. I put another code fragment below:
(define x 1)
(define (f y) (+ x y))
(set! x 2)
(f 3) ; evaluates to 5 instead of 4
Allowing such mutation is excellent. It allows you to define objects with internal state, accessible only through pre-arranged means:
(define (adder n)
(let ((x n))
(lambda (y)
(cond ((pair? y) (set! x (car y)))
(else (+ x y))))))
(define f (adder 1))
(f 5) ; 6
(f (list 10))
(f 5) ; 15
There is no way to change that x
except through the f
function and its established protocols - precisely because of lexical scoping in Scheme.
The x
variable refers to a memory cell in the internal environment frame belonging to that let
in which the internal lambda
is defined - thus returning the combination of lambda
and its defining environment, otherwise known as "closure".
And if you do not provide the protocols for mutating this internal variable, nothing can change it, as it is internal and we've long left the defining scope:
(set! x 5) ; WRONG: "x", what "x"? it's inaccessible!
EDIT: your new code, which changes the meaning of your question completely, there's no problem there as well. It is like we are still inside that defining environment, so naturally the internal variable is still accessible.
More problematic is the following
(define x 1)
(define (f y) (+ x y))
(define x 4)
(f 5) ;?? it's 9.
I would expect the second define to not interfere with the first, but R5RS says define
is like set!
in the top-level.
Closures package their defining environments with them. Top-level environment is always accessible.
The variable x
that f
refers to, lives in the top-level environment, and hence is accessible from any code in the same scope. That is to say, any code.
There are two ideas you are confusing here: scoping and indirection through memory. Lexical scope guarantees you that the reference to x
always points to the binding of x
in the let
binding.
This is not violated in your example. Conceptually, the let
binding is actually creating a new location in memory (containing 1
) and that location is the value bound to x
. When the location is dereferenced, the program looks up the current value at that memory location. When you use set!
, it sets the value in memory. Only parties that have access to the location bound to x
(via lexical scope) can access or mutate the contents in memory.
In contrast, dynamic scope allows any code to change the value you're referring to in f
, regardless of whether you gave access to the location bound to x
. For example,
(define f
(let ([x 1])
(define (f y) (+ x y))
(set! x 2)
f))
(let ([x 3]) (f 3))
would return 6
in an imaginary Scheme with dynamic scope.
No, it is not dynamic scoping. Note that your define
here is an internal definition, accessible only to the code inside the let
. In specific, f
is not defined at the module level. So nothing has leaked out.
Internal definitions are internally implemented as letrec
(R5RS) or letrec*
(R6RS). So, it's treated the same (if using R6RS semantics) as:
(let ((x 1))
(letrec* ((f (lambda (y) (+ x y))))
(set! x 2)
(f 3)))
My answer is obvious, but I don't think that anyone else has touched upon it, so let me say it: yes, it's scary. What you're really observing here is that mutation makes it very hard to reason about what your program is going to do. Purely functional code--code with no mutation--always produces the same result when called with the same inputs. Code with state and mutation does not have this property. It may be that calling a function twice with the same inputs will produce separate results.