You get a bonus - 1 coin for daily activity. Now you have 1 coin

Scope in programming

Lecture



Scope (English: scope) in programming — the part of a program within which an identifier, declared as the name of some program entity (usually a variable, a data type, or a function), remains bound to that entity, that is, allows one to refer to it by means of itself. An object's identifier is said to be «visible» at a given point in the program if, at that point, it can be used to refer to that object. Outside the scope, the same identifier may be bound to a different variable or function, or may be free (not bound to any of them). The scope may, but need not, coincide with the lifetime of the object to which the name is bound.

Identifier binding (English: binding) in the terminology of some programming languages — the process of determining the program object that an identifier gives access to at a specific point in the program and at a specific moment of its execution. This concept is essentially synonymous with scope, but can be more convenient when considering certain aspects of program execution.

Scope can also be meaningful for markup languages: for example, in HTML the scope of a control's name is the form (HTML) Scope in programming

Scope in programming

Types of scope

In a monolithic (single-module) program without nested functions and without the use of OOP, only two types of scope can exist: global and local. Other types exist only when the language has certain syntactic mechanisms.

  • Global scope — the identifier is available throughout the entire text of the program (in many languages a restriction applies — only in the text located after the declaration of this identifier).
  • Local scope — the identifier is available only inside a specific function (procedure).
  • Module-level scope can exist in modular programs consisting of several separate code fragments, usually located in different files. An identifier whose scope is the module is accessible from any code within that module.
  • Package or namespace. Within the global scope, a named subregion is artificially delimited. The name is «bound» to this part of the program and exists only within it. Outside this region the name is either completely inaccessible, or accessible in a limited way.

In OOP languages, in addition to the above, special scope restrictions may be supported that apply only to class members (identifiers declared inside a class or belonging to it:

  • Private (English: private) scope means that the name is accessible only within the methods of its own class.
  • Protected (English: protected) scope means that the name is accessible only within its own class and its descendant classes.
  • Public (English: public) scope means that the name is accessible within the scope to which its class belongs.

Ways of specifying scope

In the simplest cases, scope is determined by the location of the identifier's declaration. In cases where the location of the declaration cannot unambiguously determine the scope, special qualifiers are applied.

  • An identifier declared outside any function, procedure, or type definition is global.
  • An identifier declared inside a function definition is local to that function, that is, its scope is that function.
  • An identifier that is part of a data type definition, in the absence of additional qualifiers, has the same scope as the type identifier within whose definition it appears.
  • In languages that support modules, packages, or namespaces, an identifier declared outside all procedures and classes belongs by default to the module, package, or namespace within which its declaration is located. The boundaries of scope for a package or namespace are specified by means of special declarations, while module scope is usually limited to the current source file. A distinctive feature of this type of scope is that the language generally provides means to make the identifier accessible outside its own module (package or namespace), that is, to «extend» its scope. For this, a combination of two factors is required: the module containing the identifier must be imported with a special command at the place where it is to be used, and the identifier itself, when declared, must additionally be declared exportable. The ways of declaring an identifier exportable may vary. These may be special commands or modifiers in declarations, or naming conventions (for example, in Go, package-scope identifiers that begin with a capital letter are exportable). In a number of languages, each module (package) is artificially divided into two parts: a definitions section and an implementation section, which may be located either within a single source file (for example, in Delphi) or in different files (for example, in Modula-2); the identifiers declared in the definitions module are exportable.
  • The scope of an identifier declared inside an OOP class is by default either private or public. A different scope is assigned by means of a special declaration (for example, in C++ these are the modifiers private, public, protected) .

The list above does not exhaust all the nuances of scope determination that may exist in a particular programming language. For example, various interpretations are possible for combinations of module scope and the declared visibility of OOP class members. In some languages (for example, C++), declaring private or protected scope for a class member restricts access to it from any code that does not belong to the methods of its own class. In others (Object Pascal), all class members, including private and protected ones, are fully accessible within the module in which the class is declared, and scope restrictions apply only in other modules that import that one.

Hierarchy and disambiguation

Scopes in a program naturally form a multi-level structure, in which some scopes are nested within others. The hierarchy of scopes is usually built, at all or some levels, from the set: «global — package — module — class — local» (the specific order may differ somewhat between languages).

Packages and namespaces can have several levels of nesting, and accordingly their scopes are nested as well. The relationships between the scopes of modules and classes can differ greatly between languages. Local namespaces can also be nested, even in cases where the language does not support nested functions and procedures. For example, in C++ there are no nested functions, but each compound statement (containing a set of commands enclosed in curly braces) forms its own local scope, in which its own variables can be declared.

The hierarchical structure makes it possible to resolve ambiguities that arise when the same identifier is used in a program with more than one meaning. The search for the needed object always begins in the scope in which the code referring to the identifier is located. If an object with the required identifier is found in that scope, it is the one that is used. If there is none, the translator continues the search among the identifiers visible in the enclosing scope, and if it is not found there either — in the next level of the hierarchy.

program Example1;
var
  a,b,c: Integer; (* Global variables. *)

  procedure f1;
  var b,c: Integer  (* Local variables of procedure f1. *)
  begin
    a := 10;   (* Modifies the global a. *)
    b := 20;   (* Modifies the local b. *)
    c := 30;   (* Modifies the local c. *)
    writeln('  4:  ', a, ',', b, ',', c);
  end;

  procedure f2;
  var b,c: Integer (* Local variables of procedure f2. *)
    procedure f21;
    var c: Integer  (* Local variable of procedure f21. *)
    begin
      a := 1000;  (* Modifies the global a. *)
      b := 2000;  (* Modifies the local b of procedure f2. *)
      c := 3000;  (* Modifies the local c of procedure f21.*)
      writeln('  5:  ', a, ',', b, ',', c);
    end;
  begin
    a := 100; (* Modifies the global a. *)
    b := 200; (* Modifies the local b. *)
    c := 300; (* Modifies the local c. *)
    writeln('  6:  ', a, ',', b, ',', c);
    f21;
    writeln('  7:  ', a, ',', b, ',', c);
  end;
begin
  (* Initialization of global variables. *)
  a := 1;
  b := 2;
  c := 3;
  writeln('  1:  ', a, ',', b, ',', c);
  f1;
  writeln('  2:  ', a, ',', b, ',', c);
  f2;
  writeln('  3:  ', a, ',', b, ',', c);
end.

So, running the program above written in Pascal produces the following output:

 1:  1,2,3
 4:  10,20,30
 2:  10,2,3
 6:  100,200,300
 5:  1000,2000,3000
 7:  1000,2000,300
 3:  1000,2,3

In function f1, the variables b and c are in local scope, so changing them does not affect the global variables of the same name. Function f21 has only the variable c in its own local scope, so it modifies both the global a and b, which is local to the enclosing function f2.

Lexical vs. dynamic scope

The use of local variables — which have a limited scope and exist only within the current function — helps avoid name conflicts between two variables with the same name. However, there are two very different approaches to what it means «to be inside» a function, and accordingly two ways of implementing local scope:

  • lexical scope, or lexical context (English: lexical scope), or lexical (static) binding (English: lexical (static) binding): the local scope of a function is limited to the text of that function's definition (a variable name is meaningful within the body of the function and is considered undefined outside it).
  • dynamic scope, or dynamic context (English: dynamic scope), or dynamic binding (English: dynamic binding): the local scope is limited to the execution time function (the name is accessible while the function is executing, and disappears when the function returns control to the code that called it).

For «pure» functions, which operate only on their own parameters and local variables, lexical and dynamic scope always coincide. Problems arise when a function uses external names, for example global variables or local variables of functions it is nested within or called from. Thus, if function f calls a function g that is not nested within it, then under the lexical approach function g does not have access to the local variables of function f. Under the dynamic approach, however, function g will have access to the local variables of function f, since g was called while f was running.

For example, consider the following program:

x=1
function g () { echo $x ; x=2 ; }
function f () { local x=3 ; g ; }
f # will it print 1 or 3?
echo $x # will it print 1 or 2?

The function g() prints and modifies the value of the variable x, but this variable is neither a parameter nor a local variable in g(), meaning it must be bound to a value from the scope that g() is nested within. If the language the program is written in uses lexical scope, then the name «x» inside g() must be bound to the global variable x. The function g(), called from f(), will print the original value of the global x, after which it changes it, and the changed value will be printed by the last line of the program. That is, the program will print 1 first, then 2. Changes to the local x in the text of function f() have no effect at all on this output, since that variable is not visible either in the global scope or in function g().

If, on the other hand, the language uses dynamic scope, then the name «x» inside g() is bound to the local variable x of function f(), because g() is called from within f() and falls within its scope. Here the function g() will print the local variable x of function f() and will modify that same variable, with no effect at all on the value of the global x, so the program will print 3 first, then 1. Since in this case the program is written in bash, which uses the dynamic approach, this is indeed what happens in practice.

Both lexical and dynamic binding have their advantages and disadvantages. In practice, the choice between the two is made by the developer based both on personal preference and on the nature of the programming language being designed. Most typical high-level imperative languages, originally intended for use with a compiler (into the code of the target platform or into the bytecode of a virtual machine, it makes no difference which), implement static (lexical) scope, since it is easier to implement in a compiler. The compiler works with the lexical context, which is static and does not change during program execution, and, when processing a reference to a name, it can easily determine the memory address at which the object bound to the name is located. The dynamic context is not available to the compiler (since it can change during program execution, as the same function can be called from many places, and not always explicitly), so to provide dynamic scope the compiler must add dynamic support to the code for determining the object that the identifier refers to. This is possible, but it reduces the program's execution speed, requires additional memory, and complicates the compiler.

In the case of interpreted languages (for example, scripting languages), the situation is fundamentally different. The interpreter processes the program text directly at the moment of execution and contains internal execution-support structures, including tables of variable and function names with actual values and object addresses. It is easier and faster for the interpreter to perform dynamic binding (by a simple linear search in the identifier table) than to continuously track lexical scope. This is why interpreted languages more often support dynamic name binding.

Features of name binding

Within both the dynamic and the lexical approach to name binding, there can be nuances related to the particulars of a specific programming language or even its implementation. As an example, consider two C-like programming languages: JavaScript and Go. The languages are syntactically fairly close and both use lexical scope, but nevertheless differ in the details of its implementation.

Beginning of the scope of a local name

The following example shows two textually similar code fragments in JavaScript and Go. In both cases, a variable scope is declared in the global scope, initialized with the string «global», and inside function f() the value of scope is first printed, then a local variable with the same name is declared, initialized with the string «local», and finally the value of scope is printed once again. Below is the actual result of executing function f() in each case.

JavaScript Go
var scope = "global";
function f() {
    alert(scope); // ?
    var scope = "local";
    alert(scope);
}
var scope = "global"
func f() {
	fmt.Println(scope) // ?
	var scope = "local"
	fmt.Println(scope)
}
undefined
local
global
local

It is easy to see that the difference lies in which value is printed on the line marked with the question-mark comment.

  • In JavaScript, the scope of a local variable is the entire function, including the part that comes before its declaration; however, the initialization of that variable is carried out only at the moment the line where it appears is processed. At the time of the first call to alert(scope), the local variable scope already exists and is accessible, but has not yet been given a value, that is, according to the rules of the language, it has the special value undefined. This is precisely why «undefined» is printed on the marked line.
  • Go uses an approach more traditional for this type of language, according to which the scope of a name begins at the line where it is declared. Therefore, inside function f(), but before the declaration of the local variable scope, that variable is inaccessible, and the command marked with a question mark prints the value of the global variable scope, that is, «global».

Block scoping

Another nuance in the semantics of lexical scope is the presence or absence of so-called «block scoping», that is, the ability to declare a local variable not only inside a function, procedure, or module, but also inside a separate block of statements (in C-like languages — one enclosed in curly braces {}). Below is an example of identical code in two languages that produces different results when function f() is executed.

JavaScript Go
function f () {
	var x = 3;
	alert(x);
	for (var i = 10; i < 30; i+=10) {
		var x = i;
     	alert(x);
	}
	alert(x); // ?
}
func f() {
	var x = 3
	fmt.Println(x)
	for i := 10; i < 30; i += 10 {
		var x = i
		fmt.Println(x)
	}
	fmt.Println(x) // ?
}
3
10
20
20
3
10
20
3

The difference shows up in which value is printed by the last statement in function f(), the one marked with a question mark in the comment.

  • JavaScript has no block scoping, and re-declaring a local variable simply works as an ordinary assignment. Assigning the values of i to x inside the for loop changes the single local variable x that was declared at the start of the function. So after the loop finishes, the variable x retains the last value assigned to it in the loop. It is this value that gets printed.
  • In Go, a block of statements forms a local scope, and the variable x declared inside the loop is a new variable whose scope is only the body of the loop; it shadows the x declared at the start of the function. This «doubly local» variable receives a new value on each pass of the loop and is printed, but its changes do not affect the variable x declared outside the loop. Once the loop finishes, the x declared inside it ceases to exist, and the first x becomes visible again. Its value has remained unchanged, and it is that value which gets printed.

Visibility and existence of objects

Visibility of an identifier should not be equated with the existence of the value to which that identifier is bound. The relationship between the visibility of a name and the existence of an object is affected by the program's logic and by the object's storage class. Below are a few typical examples.

  • For variables whose memory is allocated and freed dynamically (on the heap), any relationship between visibility and existence is possible. A variable can be declared and then initialized, in which case the object corresponding to the name will actually appear later than the point at which it enters scope. But an object can also be created in advance, stored, and then assigned to the variable, that is, appear earlier. The same applies to deletion: after a delete command is called for a variable bound to a dynamic object, the variable itself remains visible, but its value no longer exists, and accessing it leads to unpredictable results. On the other hand, if the delete command is never called, the object in dynamic memory may continue to exist even after the variable referring to it has gone out of scope.
  • For local variables with static storage class (in C and C++), the value (logically) appears at the moment the program starts. However, the name is in scope only while the function containing it is executing. Moreover, the value is preserved in the intervals between calls to that function.
  • Automatic (in C terminology) variables, created on entering a function and destroyed on exiting it, exist for the same period of time during which their name is visible. That is, for them the periods of accessibility and existence can practically be considered to coincide.

Examples

C

// Global scope begins here.
int countOfUser = 0;

int main()
{
    // From this point a new scope is declared, within which the global scope is visible.
    int userNumber[10];
}
#include
int a = 0;  // global variable

int main()
{
    printf("%d", a); // prints the number 0
    {
       int a = 1; // a local variable a is declared, the global variable a is not visible
       printf("%d", a); // prints the number 1
       {
          int a = 2; // yet another local variable in the block, the global variable a is not visible, nor is the previous local variable
          printf("%d", a);  // prints the number 2
       }
    }
}

note

In C++ it was originally done poorly — friend functions
And after that things got worse, but at least it developed somehow:

  • in Delphi, private fields «leaked» within the module, then the leaks were patched using strict protected;
  • in Java, protected fields leak within the package,
  • but then its twin C# came along, in which protected does not leak, and internal was invented for controlled leaks within an assembly.


thus, when a private var is visible from the outside, that is exactly a «leak» — better to call things by their proper names.
that's probably why let and const were invented .

See also

  • [[b6216]]
  • [[b9259]]
  • [[b4565]]
  • [[b5336]]
  • [[b9259]]

See also

Comments

To leave a comment

If you have any suggestion, idea, thanks or comment, feel free to write. We really value feedback and are glad to hear your opinion.
To reply

Lectures and tutorial on "Informatics"

Terms: Informatics