Scope and extent
In Lisp there is both lexical and dynamic scoping, and the language relies on two concepts to explain bindings:
scope which is related to space
Where can you refer to this binding?
extent which is related to time
How long does this binding hold?
Lexical scope
For example, the following is a let-over-lambda that creates a closure:
(let ((a 5))
(lambda (b)
(+ a b)))
Both a
and b
are lexically scoped, meaning that you can refer to a
only under the body of the let
, and you can refer to b
only inside the lambda
.
If you store the resulting closure in memory, you can call it whenever you want: the extent is indefinite. Everytime you call the closure, the binding for a
will refer to the variable introduced by the lexically surrounding let
.
Consider this expression, let's name it F
:
(lambda (a) (lambda () a))
Here it is a function that accepts an a
, and builds a closure that constantly returns a
. If you call it twice with different bindings, you produce two different closures, as-if you did the following:
(funcall F 3)
=> (let ((a 3)) (lambda () a))
=> #<closure-1>
(funcall F 5)
=> (let ((a 5)) (lambda () a))
=> #<closure-2>
In each case a
lexically refers to a binding visible in the surrounding code, but with different values.
Dynamic scope
"Dynamic variables" (special variables in Lisp) however have:
- an indefinite scope, because you can refer to the variables from anywhere in the code
- dynamic extent: you can access the binding only while it is being defined; the binding is established while entering a block, and undone when exiting it.
For example, in Lisp special variables are written with asterisks around them (so-called "earmuffs"), like *standard-output*
which is the current output stream.
When you call:
(write "abc")
There is an optional parameter, the stream, which is bound by default to *standard-output*
, so this is as-if you wrote:
(write "abc" *standard-output*)
If you want to temporarily redirect the standard output, you can rebind it:
(let ((*standard-output* ...))
(write "abc"))
Here, the ...
is some other stream, for example a network socket or something else. While the binding is active, the standard output is redirected. When the let
exits, the variable reverts to its previous value. That's why the extent is dynamic. Notably:
(let ((*standard-output* ...))
(lambda ()
(write "abc")))
In the above example, the binding is only available while the let
body is being evaluated, but the closure that is eventually returned won't capture this binding. Later, if you call the closure, it will print using the binding for *standard-output*
that is available during the call, not at closure creation time.
The scope is indefinite because the variable is accessible from everywhere, even if there is no apparent binding (there is a global scope for special variables).
Recursion
There is no special case for recursion: a new binding shadows an existing one, which is true for both lexical and dynamic scoping. When you invoke a function recursively, the call is executed inside the existing extent of the caller, so you have access to dynamic bindings and can temporarily shadow them. Let's define a special variable named *depth*
:
(defvar *depth* 0)
You can use it to detect when some recursion is going too deep; each rebinding is only visible at its level an below, the variable is never modified, only rebound to another value.
(defun visit (tree)
(let ((*depth* (+ 1 *depth*)))
(when (> *depth* 10)
(error "Too deep"))
(when (consp tree)
(visit (car tree))
(visit (cdr tree)))))
The alternative is to change the signature of visit
to have depth
as an additional parameter, in which case you only need lexical scoping, each function call has a different binding for the variable in a given invocation of the function:
(defun visit (tree &optional (depth 0))
(when (consp tree)
(visit (car tree) (+ 1 depth))
(visit (cdr tree) (+ 1 depth))))
Note that if, however, you assign the binding to a different value, then you can have side-effects. For the special variable *depth*
, if you increment it using (setq *depth* (1+ *depth*))
then you will modifiy the nearest binding by side-effects.
For dynamic scoping, the outermost binding is the global scope, that's the level at which your b
variable is introduced: it is a global variable, that is modified with b = 2
inside the function. In that case, you can try to look at the sequence of events from a global point of view:
b
is assigned 1
rec
is called with a == 1
- then global variable
b
is assigned 2
- the recursive call enters
rec
with a == 0
- trying to access
b
here means you are reading the current value of b
in the global scope, which is 2.
Note that: if your b = 2
was a binding, and not an assignment, for exemple if you declared: let b == 2 in ...
or something that syntactically is different from an assignment, then the situation would be the same: inside the recursive call to rec
, the binding is still in effect until the code returns from the block that introduced the binding in the caller.
Conclusion
I hope this clarifies things, dynamic scope is about havine a push/pop behavior at runtime, when the code is being executed. Lexical scope is static and is directly given by how the code is written, before executing it.
b
, this would be clearer if you wroteint b = 2;
. Otherwise the assignmentb = 2
looks like it is intended to modify the global variable. $\endgroup$b
isn't local - i.e. what "lexical scoping" and "dynamic scoping" imply for how non-local scope is resolved; equivalently, what "enclosing scope" means in those cases. At least, that's how I understood it. $\endgroup$b
then the question of which variable the nameb
resolves to is trivial, and doesn't depend on the scoping rules (unlessb
is used out of scope, but that's not the case here for either lexical or dynamic scope). $\endgroup$