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) 

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.
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:
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.
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.
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.
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:
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.
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.
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.
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.
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.
// 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:
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 .
Comments