logo
 
Functions and Procedures -scopes
CptS 355 - Programming Language Design
Washington State University
Home
Calendar
Syllabus
Resources
People
Project turn-in

Name scoping

We now consider the question of "what declaration corresponds to a reference to a name from somewhere in the program?" and we begin by considering how name binding is typically implemented for a block-structured language such as C or ML.

The most important concepts in this section of the class are the notions of scope including both static scope and dynamic scope, as well as the related notions of lifetime and referencing environment.

Definition: the scope of a declaration is all the locations in the program where the name(s) bound in that declaration are visible (that is, they can be referenced). Informal type analysis of this English definition can help here. Note that scope is a function from declarations to sets of program locations. The referencing environment of a location is all of the declarations that are visible at that location. The informal type of referencing environment is a function from program locations to sets of declarations. Do not confuse scope with lifetime which is the period of time from the allocation of space for a binding to its de-allocation.

Blocks

We consider these ideas first in the context of nested blocks.
{ int x = 3;
  { int x = 5;
      { int y = x;
} } }
In this example the lifetime of each variable is the time from when its block is entered until it is exited. However, the scope of the first declaration of x does not include the declaration of y because the second declaration of x hides or shadows the first one. Another way of saying this is that there is a hole in the scope of the first declaration of x.

What is going on here: conceptually, as each block is entered a new activation record is placed on the program stack. An activation record contains locations for all of the bindings introduced in the block along with a pointer to the activation record of the previous block.

In ML each fun and val declaration introduces a new, nested block.

(Note that the book is incorrect on this point --bottom of p. 167 and top of p. 168; to see this for yourself try the following:

fun f x = g x;
fun g y = f y;
If you try to define mutually recursive functions like this you will get an error in the declaration of f saying that g is undeclared. If you want to declare mutually recursive functions use
fun f x = g x
and g y = f x;

Static and Dynamic Scope

So now we return to the basic question: which of potentially many possible declarations is referred to when an identifier is used in a program? Two possible answers seem to make sense:
Static scoping
the declaration that a use of an identifier refers to is the one in the closest enclosing block.
Dynamic scoping
the declaration that an identifer refers to is the most recent, live declaration.
In the case of nested blocks, the two definitions give the same result (you should prove this for yourself). But for nested function definitions the results are likely to be different.
val y = 4
fun g x = x+y
fun f x = let
   val y = 7
in
   g x
end
In the function g, x is called a local reference because it is declared as a parameter to g. On the other hand, y is declared outside g and this is called a non-local reference or global reference. I prefer the non-local terminology, reserving the term global for declarations at the outermost score. Both terms are in common use so its worth knowing what they mean.

In the example, using static scoping (f 10) is 14, but under dynamic scoping (f 10) would be 17. ML, Java, C++ and C all use static scoping, though for C it's pretty simple because there are no nested functions. For Java and C++ nested classes are similar to nested functions and follow similar rules.

Function implementation

Carefully study the figures in Chapter 7 to see how a stack is laid out with access links in each function activation record to allow static scoping. Remember that although we have drawn the activation records with the names of bindings in them this isn't necessary when using static scoping: the compiler can statically determine how many access links have to be followed and the offset in the activation record for any identifier reference.

Programming example for static scoping

So you may ask, how do we take advantage of static scoping when programming? Here is a small example. The ML built-in function List.filter has type ('a->bool)->'a list->'a list. It takes a function and a list and returns a sublist of elements on which the function returns true. We can use static scoping to help us implement a function that counts the number of elements in a list equal to some value.
fun count x l = let
   fun eqx y = (x=y)
in
   length (List.filter eqx l)
end
Notice how eqx here is a function of one argument that invokes the = function on two arguments, one being its own argument y and the other being the non-local argument x bound as a parameter of count. For simple functions like eqx it is often convenient to not even give them a name as in the following equivalent code:
fun count x l = 
   length (List.filter (fn y => (x=y)) l)
which is concise and quite readable (with practice).

Now you may well ask "What is the importance of static scope rules in this example? I don't see anything here that would make a difference in its behavior if dynamic scoping were used." To see the answer to this we have to consider the definition of the function List.filter. Suppose it were declared like

fun filter _ [] = []
  | filter f (x::xs) = if (f x) then x::(filter f xs)
                       else (filter f xs)
Now consider what happens if count is called using dynamic scope rules. count calls filter calls its parameter f which is bound to eqx. When eqx makes its non-local reference to x which x does it use under dynamic scoping? The one bound in the second clause of the definition of filter. Oops. That won't work. eqx will return true for every element of the list, regardless of the parameter passed to count.

Notice that this problem is insidious. The behavior of count depends on the particular choice of names in filter. If the parameter of filter were (z::zs) instead of (x::xs) then count would work correctly. It is for reasons like these that language designers have mostly concluded that the static scope rule is preferable to the dynamic scope rule.
                                                                                                                                                                                                                                                                                                                                             

  (c) 2003 Curtis Dyreson, (c) 2004, 2005 Carl H. Hauser           E-mail questions or comments to Prof. Carl Hauser