CSE 341 -- Static and Dynamic Scoping
Scope rules define the visibility rules for names in a
programming language. What if you have references to a variable named
k in different parts of the program? Do these refer to the
same variable or to different ones?
Languages such as Algol, Ada, C, and Pascal are statically scoped.
A block defines a new scope. Variables can be declared in that
scope, and aren't visible from the outside. However, variables outside the
scope -- in enclosing scopes -- are visible unless they are overridden. In
Algol and Pascal (but not C or Ada) these scope rules also apply to the
names of functions and procedures.
Static scoping is also sometimes called lexical scoping.
Simple Static Scoping Example
begin
integer m, n;
procedure hardy;
begin
print("in hardy -- n = ", n);
end;
procedure laurel(n: integer);
begin
print("in laurel -- m = ", m);
print("in laurel -- n = ", n);
hardy;
end;
m := 50;
n := 100;
print("in main program -- n = ", n);
laurel(1);
hardy;
end;
The output is:
in main program -- n = 100
in laurel -- m = 50
in laurel -- n = 1
in hardy -- n = 100 /* note that here hardy is called from laurel */
in hardy -- n = 100 /* here hardy is called from the main program */
Blocks can be nested an arbitrary number of levels deep.
Scoping in Lisp
The default scope rule in lisp is static scoping.
(setf m 50)
(setf n 100)
(defun hardy ()
(format t "~&in hardy -- n = ~a" n))
(defun laurel (n)
(format t "~&in laurel -- m = ~a" m)
(format t "~&in laurel -- n = ~a" n)
(hardy))
(format t "~&in main program -- n = ~a" n)
(laurel 1)
(hardy)
Output is the same as for Algol.
Dynamic Scoping
Common Lisp also supports dynamic scoping. Using this scoping
rule, we first look for a local definition of a variable. If it isn't
found, we look up the calling stack for a definition. (See page 435 of the
Lisp book.) Dynamic scoping was the norm in versions of Lisp before Common
Lisp, and is also used in some older, interpreted languages such as SNOBOL
and APL.
We can declare a variable as dynamically scoped in Lisp using
defvar. Example:
;; note the declaration of m and n as dynamically scoped
(defvar m 50)
(defvar n 100)
(defun hardy ()
(format t "~&in hardy -- n = ~a" n))
(defun laurel (n)
(format t "~&in laurel -- m = ~a" m)
(format t "~&in laurel -- n = ~a" n)
(hardy))
(format t "~&in main program -- n = ~a" n)
(laurel 1)
(hardy)
The output is:
in main program -- n = 100
in laurel -- m = 50
in laurel -- n = 1
in hardy -- n = 1 ;; NOTE DIFFERENCE -- here hardy is called from laurel
in hardy -- n = 100 ;; here hardy is called from the main program
Scopes and Procedures
In Algol, Pascal, Simula, and other languages in the Algol family, you can
also nest procedure and function declarations inside of other procedure and
function declarations. The same static scope rules apply. (You can't do
this in Ada or C though.)
begin
integer m, n;
procedure laurel(n: integer);
begin
procedure hardy;
begin
print("in hardy -- n = ", n);
end;
print("in laurel -- m = ", m);
print("in laurel -- n = ", n);
hardy;
end;
m := 50;
n := 100;
print("in main program -- n = ", n);
laurel(1);
/* we can't call hardy here, since the name isn't visible */
end;
Nesting procedures inside of other procedures interacts in interesting
ways with recursion:
begin
integer global, n;
procedure laurel(n: integer);
begin
procedure hardy;
begin
print(global);
print(n);
end;
if n<4 then laurel(n+1);
else hardy;
end;
global := 99;
n := 100;
laurel(1);
end;
Here the output is
99
4
Note that when we finally call hardy, there are 5 different n's on the
stack: the global one (with value 100), then 4 different invocations of
laurel (with n=1, n=2, n=3, and n=4).
Procedures as Parameters
In Algol, Pascal, and Lisp, you can pass procedures or functions as
parameters. To pass a procedure as a parameter, the system passes a
closure: a reference to the procedure body along with a pointer to
the environment of definition of the procedure.
begin
procedure test(n: integer, p: procedure);
begin
procedure rose;
begin
print("in procedure rose -- n="); print(n);
end;
print("in procedure test -- n="); print(n);
p;
if n<10 then
begin
if n=3 then test(n+1,rose) else test(n+1,p)
end
end;
procedure violet;
begin
print("in procedure violet");
end;
test(1,violet);
end;
Output:
in procedure test -- n=1
in procedure violet
in procedure test -- n=2
in procedure violet
in procedure test -- n=3
in procedure violet
in procedure test -- n=4
in procedure rose -- n=3
in procedure test -- n=5
in procedure rose -- n=3
in procedure test -- n=6
in procedure rose -- n=3
in procedure test -- n=7
in procedure rose -- n=3
in procedure test -- n=8
in procedure rose -- n=3
in procedure test -- n=9
in procedure rose -- n=3
in procedure test -- n=10
in procedure rose -- n=3
In Algol and Pascal, we can only pass procedures in as parameters -- we
can't return a procedure or a function as a value from another function.
We can do this in Lisp, however -- it means that Lisp can't always
use a stack for storage allocation for local variables.
Blocks in Smalltalk also are lexically scoped, and include their
environment of definition. Blocks can be returned from methods, assigned
to global variables, and so forth -- so that storage for local variables
can't always be allocated on a stack in Smalltalk either.
Example:
| a sum |
a := Array new: 3.
a at: 1 put: 10. a at: 2 put: 20. a at: 3 put: 30.
sum := 0.
a do: [:n | sum := sum+n].
More complicated example: suppose we evaluate the following Smalltalk code.
| k |
B1 := [k].
B2 := [:n | k := n+2].
k := 100.
After we do this, the two global variables B1 and B2 are bound to blocks.
The local variable k is no longer visible, but is still accessible and is
shared by the two blocks. So B1 value will return 100. If we
evaluate B2 value: 5, this will assign 7 to k. After that
evaluating B1 value will return 7.