A Review of Static and Dynamic Scoping in Programming Languages

All programming languages allow names to be associated with values by means of definitions, and a name is said to be in the scope of its definition. When a name is mentioned in a program, its definition (if any) must be known, in order for its invocation to make sense. However, most languages allow names to be re-defined in a program – the rules for determining to which definition a name refers, are called the scoping rules.
There are two principal scoping methods: static and dynamic. This post will firstly explain these two concepts, comparing their similarities and differences. The uses of the two methods will be discussed, illustrating these with code from actual programming languages. Finally the essay will conclude with a critical summary of the pros and cons of static and dynamic scoping for present day programming problems.

A variable’s scope…

One of the most important factors in understanding variables in programming languages is their scope. The scope of a variable is the range of statements that can access that variable and in which that variable is visible. A variable is visible within its scope and invisible or hidden outside it. Therefore, scope is typically used to define the visibility or accessibility of variables from different parts of the program.

Some languages, including C, C++, and PHP, allow a program structure that is a sequence of function definitions, in which variable definitions can appear outside the functions. Definitions outside functions create global variables which can potentially be visible to those functions. A global variable is implicitly visible in all subsequent functions, except those that include a declaration of a local variable with the same name. That is why one of the basic concepts behind scoping is to keep variables in different parts of the program distinct from one another. Since programmers often share habits about the naming of variables, the same variable name can possibly be used in multiple different scopes. The question of how to match various variable occurrences to the appropriate binding sites is generally answered by the scoping rules of a language.

The scoping rules of a language determine how a particular occurrence of a name is associated with a certain variable. In particular, scope rules determine how references to variables declared outside the currently executing subprogram or block are associated with their declarations and thus their attributes. A clear understanding of these rules for a language is therefore essential to the ability to write or read programs in that language. As mentioned above, different scoping rules affect how variables are bound to a particular scope. This has different consequences depending on the type of scoping a language uses. Typically, we can distinguish between two types of scoping methods – static and dynamic.

Static scoping…

On the one hand, in static scoping each reference to a variable is statically bound to a particular variable declaration. Programming languages that use static scoping allow for determining the scope of the variable prior to execution. This enables a compiler (and a programmer) to determine the type of every variable in the program at compile time, independently of the runtime operations. Also known as lexical scoping, static scoping is based on program text and defines the scope of a variable in terms of the lexical structure of a program. Therefore, in a language with static scoping, the bindings between names and values can be determined at compile time by just examining the program code structure, without consideration of the flow of control at run time. All variable references can be resolved by looking at the program’s source code, independently of execution.
In static scoping, in order to connect a name reference to a variable the compiler must find the corresponding declaration. The static scoping mechanism uses the nearest containing scope that declares the variable. The search process begins with first searching declarations locally, in the scope where the variable is referenced. If a declaration is found there, the name is bound to that variable. If it is not found there, then the name is looked up in increasingly larger enclosing scopes, until one is found for the given name. Enclosing static scopes (to a specific scope) are called its static ancestors, while the nearest static ancestor is called a static parent.

Generally, we can distinguish between two categories of statically-scoped languages: those in which subprograms can be nested, which create nested static scopes, and those in which subprograms cannot be nested. In case of the nested scopes, some variables may be hidden from a unit by having a “closer” variable with the same name, but some languages allow access to these hidden variables. Static scope rules are used by most traditional imperative programming languages such as Pascal and Ada as well as in modern functional languages such as ML and Haskell. It is also used in the C programming language and its syntactic and semantic relatives, although with different kinds of limitations. The following code will try to illustrate the main idea behind static scoping. It consists of an Ada procedure, Example and its nested subprograms – First and Second:

1

Suppose a reference is made to the variable X in the subprogram Second. As explained above, the correct declaration is found by first searching the declarations of Second, where the reference occurred, but no declaration for X is found there. Then, the search continues to Second’s static parent, which is Example, where the declaration for X is found. Therefore, the reference to the variable X in Second is to the X declared in the procedure Example, so 5 will be printed out on the screen. If a declaration of X was not found in Example, the search would continue to the next-larger enclosing unit, and so forth, until a declaration for X is found or the larges unit’s declarations have been searched without success. It is necessary to mention that the X declared in First is ignored, because it is not in the static ancestry of Second.

As mentioned earlier, some variable declarations can be hidden from some other code segments. For example, the variable X is declared in both Example and First. Within First, every simple reference to X is to the local X defined there (X:=1) and the outer X in Example (X:= 5) remains hidden from First. In Ada, however, hidden variables from ancestor scopes can be accessed with selective references, which include the ancestor scope’s name, such as Example.X

Dynamic scoping…

On the other hand, in a language with dynamic scope, the scope of a variable is defined in terms of program execution. That is, the bindings between names and objects depend on the flow of control at run time and, in particular, on the order in which subroutines are called. Therefore, dynamic scoping is based on the calling sequence of subprograms, not on their spatial relationship to each other as it is in static scoping. Thus, the scope of a variable can be determined only at runtime.
In comparison to the static scope rules discussed in the previous section, dynamic scope rules are generally different: the “current” binding for a given name is the one encountered most recently during execution, and not yet destroyed by returning from its scope. References to variables are connected to declarations by searching back through the chain of subprogram calls that forced execution to this point. Each variable declaration extends its effect over all subsequent statement execution, until a new declaration for the identifier is encountered. Therefore, dynamic scope uses the most recent declaration in the calling sequence. Because the flow of control cannot in general be predicted in advance, the bindings between names and values in a language with dynamic scope cannot in general be determined by a compiler. As a result, many semantic rules in a language with dynamic scope become a matter of dynamic semantics rather than static semantics. Type checking in expressions and argument checking in subroutine calls, for example, must in general be deferred until run time. To accommodate all these checks, languages with dynamic scoping tend to be interpreted rather than compiled.
Dynamic scope rules are most often used by interpreted languages such as APL and LISP. Dynamic scoping is also used in SNOBOL and Perl. Although the default scoping mechanism in Perl is static, it allows dynamically-scoped variables to be declared. For example, Perl’s keyword “my” defines a statically scoped local variable, while the keyword “local” defines a dynamically scoped local variable. The following example will try to illustrate the main idea behind dynamic scoping. It consists of a Perl code which represents two subroutines – first and second.

2

In this example, a global variable $x = 0 is present. However, there is also a local variable $x = 1 within the second () subroutine. From the print statement, second () is called first, which in turn calls first (), which prints out the requested value. It is important to notice that the keyword “local” is used to make second’s $x dynamically-scoped. Now, calling the second () subroutine results in 1 because first () sees second()’s local variable by looking down the execution stack and $x = 1 is encountered most recently during execution. If the above example used the “my” keyword instead of “local” in order to make the $x within first() statically-scoped, the calling of second() would return 0. The subroutine first() would be unable to see second()’s variable $x and would look for the global $x=0. Therefore in this case, static scoping based on the code structure would take place.

Comparison…

As seen above, static scoping and dynamic scoping are quite different from each other, although they possess some similarities. For example, dynamic scoping is like the static scoping in the sense that it first looks for the variable in the immediate scope, and then continues to look in other scopes. However, one important difference between static and dynamic scope is that finding a declaration under static scope uses the static (unchanging) relationship between blocks in the program text. In contrast, dynamic scope uses the actual sequence of calls that are executed in the dynamic (changing) execution of the program. Therefore, dynamic scoping is based on the calling sequences of program units and not on their textual layout. That is why static scoping allows the programmer to reason about object references such as parameters, variables, constants, etc. as simple name substitutions. This makes it much easier to make modular code and reason about it, since the local naming structure can be understood in isolation. In contrast, dynamic scope forces the programmer to anticipate all possible dynamic contexts in which the module’s code may be invoked.

Although most current general-purpose programming languages use static scope for declarations of variables and functions, dynamic scoping is an important concept that is used in special-purpose languages and for specialized constructs such as exceptions.
Consider the following code example in Java. It will try to compare and contrast the two scoping mechanisms and will present the different outputs that a programmer will get when the different scoping methods are used. The class Example has only one instance variable, the integer x, which is set in the constructor to the value of 0. There are two public methods – first() and second(). First() simply returns the value of x, while second() sets the value of x to 1, and then invokes the method first().

3

It is necessary to mention that Java uses static scoping by default. With this in mind, calling second() will return 0 since it has been determined at compile time that the expression x in any invocation of first() will yield the global x binding which is unaffected by the introduction of a local variable of the same name in second(). Now to illustrate the differences between the two scoping methods, imagine that Java used dynamic scoping. With dynamic scoping, the variable x will have two bindings when first() is invoked from second(): the global binding to 0, and the binding to 1 introduced in second(). Since evaluating the identifier expression by definition always yields the most recent binding, the result that the programmer will get is 1.

Final verdict…

To summarize, both dynamic and static scoping have their pros and cons. On the one hand, static scoping provides a method of nonlocal access that works well in many cases. Moreover, for nested functions that only refer to their own arguments and local variables, all relative locations can be known at compile time. Programs in static-scoped languages are easier to read, are more reliable, and execute faster than equivalent programs in dynamic-scoped languages and that is why static scoping is more widely-used in most of the programming languages. However, static scoping is not without its problems.
First, variable lookup may become slightly inefficient when very deeply lexically nested functions are used, although there are well known techniques to mitigate this.
Second, in most cases static scoping allows more access to both variables and subprograms that is necessary.
Third, since software is highly dynamic and programs that are used regularly change, there might be a problem related to program evolution. Changes in software often result in restructuring, thereby destroying the initial structure that restricted variable and subprogram access. To avoid the complexity of maintaining these access restrictions, developers usually discard structure when it gets in the way. Thus, getting around the restrictions of static scoping can lead to program designs that bear little resemblance to the original, even in areas of the program in which changes have not been made. Furthermore, designers are encouraged to use far more global variables than are necessary. All subprograms can end up being nested at the same level, in the main program, using global variables instead of deeper levels of nesting.

On the other hand, dynamic scoping remains useful in some cases. Sometimes, the parameters passed from one subprogram to another are variables that are defined in the caller. None of these need to be passed in a dynamically scoped language, because they are implicitly visible in the called subprogram. Moreover, it is worth noting that in order to create an extensible system that can completely modify itself at runtime the usage of dynamic scoping has proved very successful.However dynamic scoping also has its drawbacks.First, the correct attributes of nonlocal variables visible to a program statement cannot be determined statically. Furthermore, a reference to the name of such a variable is not always to the same variable. A statement in a subprogram that contains a reference to a nonlocal variable can refer to different nonlocal variables during different executions of the subprogram.Second, the local variables of the subprogram are all visible to any other executing subprogram, regardless of its textual proximity or how execution got to the currently executing subprogram. There is no way to protect local variables from this accessibility. As a result, dynamic scoping results in less reliable programs compared to static scoping.Third, to access nonlocal variables in dynamic-scoped languages takes far longer than to access nonlocals when static scoping is used.Finally, dynamic scoping also tends to make programs much more difficult to read and the code harder to understand. This is because the calling sequence of subprograms must be known to determine the meaning of the references to nonlocal variables. If a procedure P accesses a nonlocal constant or variable, or calls a nonlocal procedure, the effect will depend on where P was called. Indeed, P might be used in ways never anticipated by its programmer. Solving this problem can be virtually impossible for a human reader in some cases.

As seen above, understanding the static and dynamic scoping mechanisms is vital to the ability of a programmer to master the concepts of programming languages. Both scoping methods have their advantages and disadvantages and it is essential to choose the right method, when solving a particular programming problem.