2011-05-09

Programming language variables: scope and extent

This post examines two aspects of how variables work in programming languages: The scope and the extent of variables. Example source code is given in JavaScript, but should simple enough to be universally understandable.

Static versus dynamic

Before we can start our examination, we need to establish how the workings of variables can be observed. The following are two ways of looking at program code.
  • Statically, one examines the program as it exists in source code, without running it. Given the following code, we can make the static assertion that function g is nested inside function f.
        function f() {
            function g() {
                
            }
        }
    
    The adjective lexical is used synonymously with static, because both pertain to the lexicon (the words, the source) of the program.
  • Dynamically, one examines what happens while executing the program (“at runtime”). Given the following code, f and g have a dynamic relationship, because f invokes g.
        function g() {
        }
        function f() {
            g();
        }
    

Variable bindings and their scope and extent

A statement
    var x = ...;
establishes a binding for the variable x that maps the variable name to a value. In the following, the terms “variable” and “binding” are often used interchangeably. This is less precise, but makes explanations shorter. Bindings are characterized by:
  • Scope (a spatial property): Where can the binding be accessed?
  • Extent (a temporal property): How long does the binding exist?
There are several variations of implementing scope and extent in a programming language.

Scope: The direct scope of a variable is the syntactic construct “in which” a binding has been created. What constructs form scopes depends on the programming language: any kind of block in most languages, only functions in JavaScript. One has two options when it comes to determining where else a variable is accessible, in addition to the direct scope:

  • Static (lexical) scoping: A variable is accessible from the direct scope and all scopes lexically nested in it. In the following example, x is visible in its direct scope, function f, but also in the nested scope, function g.
        function f() {
            var x;
            function g(y) {
                return x + y;
            }
        }
    
  • Dynamic scoping: In dynamic scoping, a variable declared in a function is accessible in all invoked functions. The example below illustrates dynamic scoping. If f is invoked, which in turn invokes g, then myvar at (*) refers to the binding in f.
        function g() {
            var x = myvar + 1; // (*)
        }
        function f() {
            var myvar = 123;
            g();
        }
    
Most modern programming languages use static scoping. Some, such as Common Lisp, additionally support dynamic scoping (for dynamically encapsulated “global” variables). Two effects influence scoping:
  • Scopes can be nested. The direct scope can be seen as nesting all other scopes where a variable is accessible. In that case, the nested scopes are called inner scopes, the direct scope is called outer scope. In static scoping, nesting happens by nesting syntactic constructs. In dynamic scoping, nesting happens by recursively invoking functions.
  • Variables can be shadowed. If an inner scope declares a variable with the same name as a variable in an outer scope, the outer variable is shadowed by the inner one: it cannot be accessed in the inner scope (or scopes nested in it), because mentions of it refer to the inner binding. In the following example, the declaration in g shadows the declaration in f. Thus, x has the value "bar" in g and in h.
        function f() {
            var x = "foo";
            function g() {
                var x = "bar";
                function h() {
                }
            }
        }
    
Extent: the most common kinds of extent are
  • Dynamic extent: A variable exists starting with its declaration until its existence is explicitly ended. This usually means that the direct scope of the declaration has finished computing. To handle nested scopes, dynamic extent is often managed via a stack of bindings. Dynamic extent is common in simpler programming languages. JavaScript, however, can do more. Read on.
  • Indefinite extent: In JavaScript, the extent of a variable starts with its declaration. Its binding exists as long as there is code that refers to it. Thus, if a function leaves its static scope, it keeps variables in outer scopes alive if it refers to them. In the following example, function g keeps x alive, because it leaves its static scope (the function f). Later, once there are no more references to g (such as myfunc), both g and the binding for x can be garbage-collected.
        function f(x) {
            function g() {
                return x;
            }
            return g;
        }
        var myfunc = f("hello");
        console.log(myfunc()); // output: hello
    
Note that there exist many different definitions of “extent” in the literature. We have covered just one of them [1].

Related reading

  1. Scope and Extent (chapter 3 in “Common Lisp the Language” by Guy Steele). This chapter is the source of part of this post.

No comments: