Dynamic and Lexical Scopes
This seems like it should work, and it even worked on a few examples, except for one which was hard to follow. Seems like we have a bug…
Now we get to a tricky issue that managed to be a problem for lots of language implementors, including the first version of Lisp. Lets try to run the following expression — try to figure out what it will evaluate to:
{with {f {fun {y} {+ x y}}}
{with {x 5}
{call f 4}}}}")
We expect it to return 7
(at least I do!), but we get 9
instead…
The question is — should it return 9
?
What we have arrived to is called dynamic scope. Scope is determined by the dynamic run-time environment (which is represented by our substitution cache). This is almost always undesirable, as I hope to convince you.
Before we start, we define two scope options for a programming language:
-
Static Scope (also called Lexical Scope): In a language with static scope, each identifier gets its value from the scope of its definition, not its use.
-
Dynamic Scope: In a language with dynamic scope, each identifier gets its value from the scope of its use, not its definition.
Racket uses lexical scope, our new evaluator uses dynamic, the old substitution-based evaluator was static etc.
As a side-remark, Lisp began its life as a dynamically-scoped language. The artifacts of this were (sort-of) dismissed as an implementation bug. When Scheme was introduced, it was the first Lisp dialect that used strictly lexical scoping, and Racket is obviously doing the same. (Some Lisp implementations used dynamic scope for interpreted code and lexical scope for compiled code!) In fact, Emacs Lisp is the only live dialects of Lisp that is still dynamically scoped by default. To see this, compare a version of the above code in Racket:
(let ((f (lambda (y) (+ x y))))
(let ((x 5))
(f 4))))
and the Emacs Lisp version (which looks almost the same):
(let ((f (lambda (y) (+ x y))))
(let ((x 5))
(funcall f 4))))
which also happens when we use another function on the way:
(funcall func val))
(let ((x 3))
(let ((f (lambda (y) (+ x y))))
(let ((x 5))
(blah f 4))))
and note that renaming identifiers can lead to different code — change
that val
to x
:
(funcall func x))
(let ((x 3))
(let ((f (lambda (y) (+ x y))))
(let ((x 5))
(blah f 4))))
and you get 8
because the argument name changed the x
that the
internal function sees!
Consider also this Emacs Lisp function:
x)
which has no meaning by itself (x
is unbound),
but can be given a dynamic meaning using a let
:
or a function application:
(return-x))
(foo 5)
There is also a dynamically-scoped language in the course languages:
(define x 123)
(define (getx) x)
(define (bar1 x) (getx))
(define (bar2 y) (getx))
(test (getx) => 123)
(test (let ([x 456]) (getx)) => 456)
(test (getx) => 123)
(test (bar1 999) => 999)
(test (bar2 999) => 123)
(define (foo x) (define (helper) (+ x 1)) helper)
(test ((foo 0)) => 124)
;; and *much* worse:
(define (add x y) (+ x y))
(test (let ([+ *]) (add 6 7)) => 42)
Note how bad the last example gets: you basically cannot call any function and know in advance what it will do.
There are some cases where dynamic scope can be useful in that it allows you to “remotely” customize any piece of code. A good example of where this is taken to an extreme is Emacs: originally, it was based on an ancient Lisp dialect that was still dynamically scoped, but it retained this feature even when practically all Lisp dialects moved on to having lexical scope by default. The reason for this is that the danger of dynamic scope is also a way to make a very open system where almost anything can be customized by changing it “remotely”. Here’s a concrete example for a similar kind of dynamic scope usage that makes a very hackable and open system:
(define tax% 6.25)
(define (with-tax n)
(+ n (* n (/ tax% 100))))
(with-tax 10) ; how much do we pay?
(let ([tax% 17.0]) (with-tax 10)) ; how much would we pay in Israel?
;; make that into a function
(define il-tax% 17.0)
(define (ma-over-il-saving n)
(- (let ([tax% il-tax%]) (with-tax n))
(with-tax n)))
(ma-over-il-saving 10)
;; can even control that: how much would we save if
;; the tax in israel went down one percent?
(let ([il-tax% (- il-tax% 1)]) (ma-over-il-saving 10))
;; or change both: how much savings in NH instead of MA?
(let ((tax% 0.0) (il-tax% tax%))
(ma-over-il-saving 1000))
Obviously, this power to customize everything is also the main source of problems with getting no guarantees for code. A common way to get the best of both worlds is to have controllable dynamic scope. For example, Common Lisp also has lexical scope everywhere by default, but some variables can be declared as special, which means that they are dynamically scoped. The main problem with that is that you can’t tell when a variable is special by just looking at the code that uses it, so a more popular approach is the one that is used in Racket: all bindings are always lexically scoped, but there are parameters which are a kind of dynamically scoped value containers — but they are bound to plain (lexically scoped) identifiers. Here’s the same code as above, translated to Racket with parameters:
(define tax% (make-parameter 6.5)) ; create the dynamic container
(define (with-tax n)
(+ n (* n (/ (tax%) 100)))) ; note how its value is accessed
(with-tax 10) ; how much do we pay?
(parameterize ([tax% 17.0]) (with-tax 10)) ; not a `let'
;; make that into a function
(define il-tax% (make-parameter 17.0))
(define (ma-over-il-saving n)
(- (parameterize ([tax% (il-tax%)]) (with-tax n))
(with-tax n)))
(ma-over-il-saving 10)
(parameterize ([il-tax% (- (il-tax%) 1)]) (ma-over-il-saving 10))
The main point here is that the points where a dynamically scoped value
is used are under the programmer’s control — you cannot “customize”
what -
is doing, for example. This gives us back the guarantees that
we like to have (= that code works), but of course these points are
pre-determined, unlike an environment where everything can be customized
including things that are unexpectedly useful.
As a side-note, after many decades of debating this, Emacs has finally added lexical scope in its core language, but this is still determined by a flag — a global
lexical-binding
variable.
Dynamic versus Lexical Scope
And back to the discussion of whether we should use dynamic or lexical scope:
-
The most important fact is that we want to view programs as executed by the normal substituting evaluator. Our original motivation was to optimize evaluation only — not to change the semantics! It follows that we want the result of this optimization to behave in the same way. All we need is to evaluate:
(run "{with {x 3}
{with {f {fun {y} {+ x y}}}
{with {x 5}
{call f 4}}}}")in the original evaluator to get convinced that
7
should be the correct result (note also that the same code, when translated into Racket, evaluates to7
).(Yet, this is a very important optimization, which without it lots of programs become too slow to be feasible, so you might claim that you’re fine with the modified semantics…)
-
It does not allow using functions as objects, for example, we have seen that we have a functional representation for pairs:
(define (kons x y)
(lambda (n)
(match n
['first x]
['second y]
[else (error ...)])))
(define my-pair (kons 1 2))If this is evaluated in a dynamically-scoped language, we do get a function as a result, but the values bound to
x
andy
are now gone! Using the substitution model we substituted these values in, but now they were only held in a cache which no has no entries for them…In the same way, currying would not work, our nice
deriv
function would not work etc etc etc. -
Makes reasoning impossible, because any piece of code behaves in a way that cannot be predicted until run-time. For example, if dynamic scoping was used in Racket, then you wouldn’t be able to know what this function is doing:
(define (foo)
x)As it is, it will cause a run-time error, but if you call it like this:
(let ([x 1])
(foo))then it will return
1
, and if you later do this:(define (bar x)
(foo))
(let ([x 1])
(bar 2))then you would get
2
!These problems can be demonstrated in Emacs Lisp too, but Racket goes one step further — it uses the same rule for evaluating a function as well as its values (Lisp uses a different name-space for functions). Because of this, you cannot even rely on the following function:
(define (add x y)
(+ x y))to always add
x
andy
! — A similar example to the above:(let ([+ -])
(add 1 2))would return
-1
! -
Many so-called “scripting” languages begin their lives with dynamic scoping. The main reason, as we’ve seen, is that implementing it is extremely simple (no, nobody does substitution in the real world! (Well, almost nobody…)).
Another reason is that these problems make life impossible if you want to use functions as object like you do in Racket, so you notice them very fast — but in a
normal
language without first-class functions, problems are not as obvious. -
For example, bash has
local
variables, but they have dynamic scope:x="the global x"
print_x() { echo "The current value of x is \"$x\""; }
foo() { local x="x from foo"; print_x; }
print_x; foo; print_xPerl began its life with dynamic scope for variables that are declared
local
:$x="the global x";
sub print_x { print "The current value of x is \"$x\"\n"; }
sub foo { local($x); $x="x from foo"; print_x; }
print_x; foo; print_x;When faced with this problem, “the Perl way” was, obviously, not to remove or fix features, but to pile them up — so
local
still behaves in this way, and now there is amy
declaration which achieves proper lexical scope (and every serious Perl programmer knows that you should always usemy
)…There are other examples of languages that changed, and languages that want to change (e.g, nobody likes dynamic scope in Emacs Lisp, but there’s just too much code now).
-
This is still a tricky issue, like any other issue with bindings. For example, googling got me quickly to a Python blog post which is confused about what “dynamic scoping” is… It claims that Python uses dynamic scope (Search for “Python uses dynamic as opposed to lexical scoping”), yet python always used lexical scope rules, as can be seen by translating their code to Racket (ignore side-effects in this computation):
(define (orange-juice)
(* x 2))
(define x 3)
(define y (orange-juice)) ; y is now 6
(define x 1)
(define y (orange-juice)) ; y is now 2or by trying this in Python:
def orange_juice():
return x*2
def foo(x):
return orange_juice()
foo(2)The real problem of python (pre 2.1, and pre 2.2 without the funny
from __future__ import nested_scopeline) is that it didn’t create closures, which we will talk about shortly.
-
Another example, which is an indicator of how easy it is to mess up your scope is the following Ruby bug — running in
irb
:% irb
irb(main):001:0> x = 0
=> 0
irb(main):002:0> lambda{|x| x}.call(5)
=> 5
irb(main):003:0> x
=> 5(This is a bug due to weird scoping rules for variables, which was fixed in newer versions of Ruby. See this Ruby rant for details, or read about Ruby and the principle of unwelcome surprise for additional gems (the latter is gone, so you’ll need the web archive to read it).)
-
Another thing to consider is the fact that compilation is something that you do based only on the lexical structure of programs, since compilers never actually run code. This means that dynamic scope makes compilation close to impossible.
-
There are some advantages for dynamic scope too. Two notable ones are:
-
Dynamic scope makes it easy to have a “configuration variable” easily change for the extent of a calling piece of code (this is used extensively in Emacs, for example). The thing is that usually we want to control which variables are “configurable” in this way, statically scoped languages like Racket often choose a separate facility for these. To rephrase the problem of dynamic scoping, it’s that all variables are modifiable.
The same can be said about functions: it is sometimes desirable to change a function dynamically (for example, see “Aspect Oriented Programming”), but if there is no control and all functions can change, we get a world where no code can every be reliable.
-
It makes recursion immediately available — for example,
{with {f {fun {x} {call f x}}}
{call f 0}}is an infinite loop with a dynamically scoped language. But in a lexically scoped language we will need to do some more work to get recursion going.
-