Scope of variables
The visibility area (or for short: scope) of a variable is the part of the code within which the variable is accessible. By limiting scopes, you can avoid variable name conflicts. The meaning is very simple: two different functions can have arguments with the same name x', and these two arguments `x
refer to different objects. There may be many other situations where the same name has different meanings in different code blocks. In which cases variables with the same name have the same or different values depends on the scope definition rules. They are discussed in detail in this section.
With the help of certain language constructs, _blocks are formed by scopes, that is, parts of the code that can act as scopes for certain sets of variables. The scope of a variable cannot be an arbitrary set of source code lines; the scope must correspond to one of these blocks. There are two main types of areas in Julia: global and local. The second type can be nested. Julia also distinguishes between designs that introduce strict areas as opposed to non-strict ones. The opportunity depends on it. https://en.wikipedia.org/wiki/Variable_shadowing [shadowing] a global variable with the same name.
Field of view designs
The area blocks are created using the following constructions.
Construction | Type of area | The area where you can use |
---|---|---|
Global |
Global |
|
Local (non-strict) |
Global |
|
Local (non-strict) |
Global, Local |
|
Local (strict) |
Global |
|
Local (strict) |
Global, Local |
It is noteworthy that this table does not begin blocks and if blocks that do not form new areas. There are slightly different rules for these three types of areas, which will be discussed later.
Julia uses https://en.wikipedia.org/wiki/Scope_ (computer_science)#Lexical_scope_vs._dynamic_scope[lexical areas]. This means that the scope of the function is inherited not from the scope of the caller, but from the scope in which the function is defined. For example, in the following code, the variable x
inside foo
refers to x
in the global scope of the module Bar
:
julia> module Bar
x = 1
foo() = x
end;
and not on the x
in the area where foo
is used:
julia> import .Bar
julia> x = -1;
julia> Bar.foo()
1
Thus, the "lexical scope" means that it is possible to determine what a variable refers to in a particular place of the code based only on the code where it is used. The execution of the program does not affect this. The nested area has variables available from all the external areas in which it is nested. In turn, variables from the inner regions are not available in the outer regions.
Global viewport
Each module forms a new global domain, independent of the global domains of the other modules - there is no comprehensive global domain. Variables from other modules can be entered into the module area using the operators using and import, as well as access them by qualified name through dot notation. That is, each module is a so-called namespace, as well as a first-class data structure that associates names with values.
If a top-level expression contains a variable declaration with the keyword local
, then this variable is not available outside of this expression. A variable inside an expression does not affect global variables with the same name. An example is the declaration of local x
in the begin
or if
block at the top level.
julia> x = 1
begin
local x = 0
@show x
end
@show x;
x = 0
x = 1
Keep in mind that the interactive command prompt (REPL) belongs to the global scope of the Main
module.
Local viewport
Most of the code blocks form new local areas (for a complete list, see table above). If such a block is syntactically nested in another local area, the area it creates will be nested in all the local areas it belongs to. In turn, these areas are eventually nested in the global area of the module in which the code is calculated. Variables from external areas are visible in any internal area they contain (that is, they can be read and written to it), unless a local variable with the same name "obscures" the external one. This is true even if the external local variable is declared after the internal block (that is, later in the program text). When we say that a variable exists in a particular area, we mean that a variable with this name exists in any of the areas in which the current area is nested, or in the current area itself.
In some programming languages, variables must be explicitly declared before they can be used. In Julia, explicit declaration is also possible: if you write local x
in any local scope, a new local variable will be declared in it, regardless of whether there is already a variable named x
in the external scope. However, declaring each new variable in this way is tedious, so in Julia, as in many other languages, when assigning a value to a previously unknown variable, this variable is declared implicitly. If the current scope is global, then the new variable will be global. If the current area is local, then the new variable will be local to the most deeply nested local area and accessible only within it, but not from the outside. When assigning a value to an existing local variable, it is always the local variable that changes: you can only shade a local variable by explicitly declaring a new local variable in a nested scope using the keyword local
. In particular, this applies to variables that are assigned values in internal functions, which may be unexpected for Python developers. In this language, an assignment in an internal function results in the creation of a new local variable, unless the variable is explicitly declared as non-local.
In general, such a mechanism is quite natural to understand, but, like many natural phenomena, it has its own subtleties that are not obvious at first glance.
When the expression x = <value>
is used in the local scope, its value is determined in Julia according to the following rules, taking into account where the assignment expression is located and what x
already refers to in this part of the code.
-
An existing local variable: if
x
is already a local variable, then the value is assigned to this existing variable `x'. -
Strict area: if the local variable
x
no longer exists and the assignment takes place inside the strict scope structure (i.e. inside thelet
block, function or macro body, inclusion or generator), a new local variable namedx
is created in the assignment scope. -
Non-strict area: if the local variable
x
_ does not exist yet_ and all the constructions of the fields that the assignment includes are non-strict (loops, 'try` andcatch
blocks orstruct
blocks), the result depends on whether the global variablex
is defined.-
If the global variable
x
is not defined, a new local variable namedx
is created in the assignment scope. -
If the global variable
x
_ is defined, the assignment is considered ambiguous.-
In non-interactive contexts_ (files, eval) an ambiguity warning is displayed, and a new local variable is created.
-
In interactive contexts (REPL, notebooks), the value is assigned to the global variable
x
.
-
-
As you can see, in non-interactive contexts, the rules for strict and non-strict scopes are the same, except that if a global variable is obscured by an implicit local variable (i.e., not declared as local x
), a warning is displayed. In interactive contexts, more complex rules apply for convenience. They will be discussed in more detail in further examples.
Now that you know the rules, let’s look at some examples. It is assumed that each example is executed in a new REPL session, so that in each code fragment, values are assigned only to global variables in this block.
Let’s start with a simple and clear situation - assignments inside a strict scope, in this case inside the body of a function, when a local variable with that name does not yet exist.:
julia> function greet()
x = "hello" # Новая локальная переменная
println(x)
end
greet (generic function with 1 method)
julia> greet()
hello
julia> x # Глобальная
ERROR: UndefVarError: `x` not defined in `Main`
Within the greet
function, the assignment x = "hello"
creates a new local variable x
in the function scope. In this case, two facts are important: the assignment takes place in the local area, and the local variable x
does not exist yet. Since x
is a local variable, it doesn’t matter if there is a global variable named x'. Here, for example, we perform the assignment `x = 123
before defining and calling `greet'.
julia> x = 123 # Глобальная
123
julia> function greet()
x = "hello" # Новая локальная переменная
println(x)
end
greet (generic function with 1 method)
julia> greet()
hello
julia> x # Глобальная
123
Since the variable x
in greet
is local, calling greet
does not affect the value of the global variable x
(and it does not matter if it exists). According to the strict scope rule, the existence of a global variable named x
is ignored: the assignment of a value to the variable x
in the strict scope occurs at the local level (unless the variable x
is declared global).
In the next obvious situation that we will consider, a local variable named x
already exists. In this case, the operator x = <value>
always assigns a value to this existing local variable `x'. This is true regardless of whether the assignment occurs in the same local scope, in an internal local scope in the body of the same function, or in the body of a function nested in another function, which is also known as https://en.wikipedia.org/wiki/Closure_ (computer_programming)[closing].
For example, we use the sum_to
function, which calculates the sum of integers from one to `n'.
function sum_to(n)
s = 0 # Новая локальная переменная
for i = 1:n
s = s + i # Значение присваивается существующей локальной переменной
end
return s # Та же локальная переменная
end
As in the previous example, the first assignment of the variable s
at the beginning of the function sum_to
leads to the declaration of a new local variable s
in the body of the function. The 'for` loop has its own internal local scope within the function scope. In the place where the assignment s = s + i
occurs, the local variable s
already exists, so the assignment causes the value of the existing variable s
to change, rather than creating a new local variable. You can check this by calling `sum_to' in the REPL.
julia> function sum_to(n)
s = 0 # Новая локальная переменная
for i = 1:n
s = s + i # Значение присваивается существующей локальной переменной
end
return s # Та же локальная переменная
end
sum_to (generic function with 1 method)
julia> sum_to(10)
55
julia> s # Глобальная
ERROR: UndefVarError: `s` not defined in `Main`
Since the variable s
is local to the function sum_to
, calling the function does not affect the global variable s
. In addition, you can see that the expression s = s + i
in the for
loop modifies the same variable s
that was created as a result of initializing s = 0
, since we get the correct sum of 55 for integers from 1 to 10.
Let’s focus a little on the fact that the for
loop has its own scope, and write a slightly more detailed version of the function, which we’ll call sum_to_def'. In it, the sum of `s + i
is stored in the variable t
before changing s
.
julia> function sum_to_def(n)
s = 0 # Новая локальная переменная
for i = 1:n
t = s + i # Новая локальная переменная `t`
s = t # Значение присваивается существующей локальной переменной `s`
end
return s, @isdefined(t)
end
sum_to_def (generic function with 1 method)
julia> sum_to_def(10)
(55, false)
This version returns s
as before, but also uses the macro @isdefined
to return a boolean value indicating whether there is a local variable named t
defined in the external local scope of the function. As can be seen from the result, the variable t
is not defined outside the body of the for
loop. The reason again lies in the strict domain rule.: since the assignment of a value to the variable t
occurs inside a function that introduces a strict scope, it leads to the creation of a new local variable t
in the local scope where it appears, that is, inside the body of the loop. Even if there was a global variable named t
, there would be no difference — the strict scope rule is not affected by the contents of the global scope.
Note that the local area of the body of the for loop does not differ from the local area of the internal function. This means that this example could have been rewritten differently by implementing the body of the loop as a call to an internal auxiliary function. The behavior would not have changed.
julia> function sum_to_def_closure(n)
function loop_body(i)
t = s + i # Новая локальная переменная `t`
s = t # Значение присваивается той же локальной переменной `s`, что и ниже
end
s = 0 # Новая локальная переменная
for i = 1:n
loop_body(i)
end
return s, @isdefined(t)
end
sum_to_def_closure (generic function with 1 method)
julia> sum_to_def_closure(10)
(55, false)
This example illustrates a couple of important points.
-
The internal function areas are similar to any other nested local area. In particular, if a variable is already local outside the internal function and is assigned a value in the internal function, the external local variable is changed.
-
It doesn’t matter if the definition of an external local variable is located after the place where the variable is updated, the rule remains the same. Before determining the value of internal local variables, the entire external local domain is analyzed and its local variables are resolved.
This scheme means that code can usually be transferred to or from an internal function without changing its meaning, which simplifies the use of a number of standard idioms in a language that supports closures (see the section on do blocks).
Let’s move on to a number of more confusing cases where the lax domain rule applies. To do this, we will extract the bodies of the greet
and sum_to_def
functions into the context of a non-strict scope. First, let’s put the body of the greet
function in the for
loop, which forms a non-strict scope, and execute it in the REPL.
julia> for i = 1:3
x = "hello" # Новая локальная переменная
println(x)
end
hello
hello
hello
julia> x
ERROR: UndefVarError: `x` not defined in `Main`
Since the global variable x
is not defined at the time of execution of the for
loop, the first provision of the rule applies for a non-strict scope and the variable x
is created as a local for the for
loop. Therefore, after the loop is executed, the global variable x
remains undefined. Next, let’s extract the body of the function sum_to_def
into the global scope and fix its argument as `n = 10'.
s = 0
for i = 1:10
t = s + i
s = t
end
s
@isdefined(t)
What does this code do? Hint: This is a trick question. The correct answer is that it all depends on the circumstances. If this code is entered interactively, it is executed in the same way as in the function body. However, if the code is executed from a file, an ambiguity warning is displayed and the error "variable not defined" occurs. Let’s check how it works in the REPL first.
julia> s = 0 # Глобальная
0
julia> for i = 1:10
t = s + i # Новая локальная переменная `t`
s = t # Значение присваивается глобальной переменной `s`
end
julia> s # Глобальная
55
julia> @isdefined(t) # Глобальная
false
In REPL, execution in the function body is simulated by determining whether an assignment within a loop results in changing the value of a global variable or creating a new local variable based on whether a global variable with that name is defined. If a global variable with that name exists, the assignment causes it to be changed. If there is no global variable, a new local variable is created as a result of the assignment. This example demonstrates both cases.
-
There is no global variable named
t
, therefore, as a result of the assignmentt = s + i
, a new variablet
is created, local to the 'for` loop. -
A global variable named
s
exists, so the expressions = t
assigns a value to it.
The second fact explains why the value of the global variable s
changes as a result of the execution of the loop, and the first is why the variable t
is still undefined after the execution of the loop. Now let’s try to execute the same code as if it were contained in a file.
julia> code = """
s = 0 # Глобальная
for i = 1:10
t = s + i # Новая локальная переменная `t`
s = t # Новая локальная переменная `s` с предупреждением
end
s, # Глобальная
@isdefined(t) # Глобальная
""";
julia> include_string(Main, code)
┌ Warning: Assignment to `s` in soft scope is ambiguous because a global variable by the same name exists: `s` will be treated as a new local. Disambiguate by using `local s` to suppress this warning or `global s` to assign to the existing global variable.
└ @ string:4
ERROR: LoadError: UndefVarError: `s` not defined in local scope
Here we use the function include_string
to calculate the code' as the file content. You could also save the `code
to a file, and then call include
for that file — the result would be the same. As you can see, the code in this case is executed in a completely different way than in the REPL. Let’s figure out what’s going on here.
-
Before executing the loop, the global variable
s
with the value0
is defined. -
The assignment of
s = t
occurs in a non-strict domain — in thefor
loop outside the body of any function or other construct forming a strict domain. -
Therefore, the second provision of the rule applies for a non-strict domain and the assignment is ambiguous, resulting in a warning.
-
Execution continues, and the
s
variable becomes local to the body of the 'for` loop. -
Since the variable
s
is local to thefor
loop, it is not defined when calculatingt = s + i
and an error occurs. -
This completes the execution, but if it had continued until
s
and@isdefined(t)
, the values0
and `false' would have been returned.
This example demonstrates a number of important aspects related to scopes: within a scope, each variable can have only one value, which does not depend on the order of expressions. The presence of the expression s = t
in the loop makes the variable s
local to it. This means that it is also local when used on the right side of the expression t = s + i
, although this expression is evaluated first. One might think that the s
in the first line of the loop would be a global variable, and the `s' in the second line would be a local one, but this is impossible, since these two lines are in the same block of the scope, and within a certain scope each variable can have only one meaning.
Learn more about non-strict scopes
We have reviewed all the rules for local areas, but before concluding this section, it is worth saying a few words about why a non-strict area acts differently in interactive and non-interactive contexts. Two obvious questions arise.
-
Why is the behavior not the same everywhere as in the REPL?
-
Why is the behavior not the same everywhere as in files? Maybe the warning can just be ignored?
In versions of Julia up to and including 0.6, all global scopes worked as they do now in REPL: when the assignment of x = <value>
occurred in a loop (either in the try
/catch
block or in the struct
body), but outside the function body (either in the let
block or inclusion), the decision on whether the variable x
should be local to the loop was made depending on the presence of a global variable named `x'. The advantage of this approach is that it is intuitive and convenient, as it is as similar as possible to the behavior inside the function body. In particular, it makes it easy to transfer code from the function body to the REPL and vice versa when debugging the function. But there are also disadvantages. Firstly, this approach is difficult to explain and understand: it often confused users and caused complaints. Quite fair. Secondly, and more importantly, it creates difficulties when writing large-scale programs. In a small piece of code like the following, it’s not difficult to understand what’s going on.
s = 0
for i = 1:10
s += i
end
Obviously, the goal is to change the existing global variable `s'. What else could it mean? However, in reality, the code is not always so short and clear. For example, in real programs, you can often find this:
x = 123
# гораздо дальше
# возможно, в другом файле
for i = 1:10
x = "hello"
println(x)
end
# гораздо дальше
# возможно, еще в одном файле
# или, может быть, в предыдущем, где `x = 123`
y = x + 234
It’s not so easy to figure out what’s going on here. Since x + "hello"
will cause a method error, it seems likely that the intention was to make the variable x
local to the 'for` loop. However, based on the values of the execution time and existing methods, it is impossible to determine the scope of variables. The behavior adopted in versions of Julia up to and including 0.6 was especially dangerous for the following reason: someone could first write a for
loop, and it worked fine, but then someone else could add a new global variable in a completely different place, possibly in a different file, and the meaning of the code changed. It either just started to fail, or, even worse, it was executed, but with the wrong result. In well-designed programming languages, this effect https://en.wikipedia.org/wiki/Action_at_a_distance_ (computer_programming)[creepy ranged attacks] should not be allowed.
Therefore, in Julia 1.0, we simplified the rules regarding scopes: in any local scope, assigning a value to a name that is not yet a local variable leads to the creation of a new local variable. As a result, the concept of a non-strict area has completely disappeared, which made it possible to prevent possible negative effects of long-range action. We have identified and eliminated many errors that have become our punishment for abandoning non-strict areas. And finally, they could rest on their laurels! But no, it’s not that simple. After all, I often had to do this now.:
s = 0
for i = 1:10
global s += i
end
Do you see the global
annotation? It’s horrible. Of course, this state of affairs could not be tolerated. But seriously, the mandatory use of global
in such top-level code is fraught with two main problems.
-
Now it is not so convenient to copy the code from the function body to the REPL for debugging - you have to add
global
annotations, and then delete them when transferring the code back. -
Inexperienced developers can write such code without the keyword
global
and not understand why it does not work, - the error "variables
is not defined" is returned, which does not shed much light on the cause of the problem.
Starting with Julia 1.5, this code works without the global
annotation in interactive contexts such as REPL or Jupyter notebooks (as in Julia 0.6), and in files and other non-interactive contexts, such an unambiguous warning is displayed.
Assigning a value to the variable
s
in a non-strict scope is ambiguous because there is a global variable with the same name. Thes
will be interpreted as a new local variable. To resolve the ambiguity, uselocal s
to suppress this warning orglobal s
to assign a value to an existing global variable.
This solves both problems and preserves the advantages of writing large-scale programs implemented in version 1.0: global variables do not have a hidden effect on the meaning of deleted code; debugging by copying to REPL works, and novice developers have no problems; when someone forgets to add the global
annotation or accidentally obscures an existing global variable. local in a non-strict area, a clear warning is issued.
An important feature of this approach is that the code that runs in the file without displaying a warning will work the same way in a new REPL session. Conversely, if, after saving the code from the REPL session in a file, it is executed differently than in the REPL, a warning will be received.
Let blocks
The 'let` operator creates a block with a strict scope (see above) and introduces new variable bindings each time it is executed. The value of the variable must be assigned immediately.
julia> var1 = let x
for i in 1:5
(i == 4) && (x = i; break)
end
x
end
4
While an existing value location can be overwritten with a new value when assigned, let
always creates a new location. This difference usually does not play a role and manifests itself only if variables continue to exist outside their scope due to closure. The let
syntax allows for a comma-separated list of variable assignments and names.:
julia> x, y, z = -1, -1, -1;
julia> let x = 1, z
println("x: $x, y: $y") # x — локальная переменная, y — глобальная
println("z: $z") # выдает ошибку, так как переменной z еще не присвоено значение, но она является локальной
end
x: 1, y: -1
ERROR: UndefVarError: `z` not defined in local scope
Assignment operators are evaluated in order, with the right side of the operator being evaluated within the scope before introducing a new variable on the left side. For this reason, the expression let x = x
will make sense, since the x
in the left and right parts are different variables that are stored separately. Here is an example that requires the let
behavior.
julia> Fs = Vector{Any}(undef, 2); i = 1;
julia> while i <= 2
Fs[i] = ()->i
global i += 1
end
julia> Fs[1]()
3
julia> Fs[2]()
3
Two closures are created and saved here, returning the variable i'. However, it is always the same variable `i
, so closures work the same way. Using let
, you can create a new binding for `i'.
julia> Fs = Vector{Any}(undef, 2); i = 1;
julia> while i <= 2
let i = i
Fs[i] = ()->i
end
global i += 1
end
julia> Fs[1]()
1
julia> Fs[2]()
2
Since the 'begin` construction does not introduce a new scope, it may be useful to use let
without arguments to simply introduce a new scope block without creating new bindings immediately.
julia> let
local x = 1
let
local x = 2
end
x
end
1
Since let
introduces a new scope block, the internal local variable x
is different from the external local x
. This particular example is equivalent to the following code.
julia> let x = 1
let x = 2
end
x
end
1
Cycles and inclusions
In cycles and inclusions the variables entered in their bodies are re-placed in memory at each iteration, as if the body of the loop were enclosed in a `let' block. For example:
julia> Fs = Vector{Any}(undef, 2);
julia> for j = 1:2
Fs[j] = ()->j
end
julia> Fs[1]()
1
julia> Fs[2]()
2
The for
loop iteration or inclusion variable is always a new variable.:
julia> function f()
i = 0
for i = 1:3
# пусто
end
return i
end;
julia> f()
0
However, sometimes it is useful to reuse an existing local variable as an iteration variable. It is convenient to use the keyword outer
for this purpose.
julia> function f()
i = 0
for outer i = 1:3
# пусто
end
return i
end;
julia> f()
3
Constants
Variables are often used to assign names to certain immutable values. Values for such variables are assigned only once. You can use a keyword to inform the compiler about this assignment. const
.
julia> const e = 2.71828182845904523536;
julia> const pi = 3.14159265358979323846;
Multiple variables can be declared in a single const
statement.
julia> const a, b = 1, 2
(1, 2)
The declaration const
should only be used in the global scope in relation to global variables. It is difficult for the compiler to optimize code with global variables, since their values (or even types) can change at any moment. If the global variable does not change, declaring const
solves this performance problem.
The situation is different with local constants. The compiler can automatically determine if a local variable is a constant, so you don’t need to declare local constants. Moreover, this feature is not supported yet.
Special top-level assignments, such as those performed by the keywords function
and struct
, are constant by default.
Note that const
only affects the binding of a variable; a variable can be bound to a mutable object (for example, an array) that can still be modified. In addition, when trying to assign a value to a variable declared as a constant, the following scenarios are possible.
-
If the type of the new value differs from the type of the constant, an error occurs.
julia> const x = 1.0
1.0
julia> x = 1
ERROR: invalid redefinition of constant x
-
If the new value and the constant are of the same type, a warning is displayed.
julia> const y = 1.0
1.0
julia> y = 2.0
WARNING: redefinition of constant y. This may fail, cause incorrect answers, or produce other errors.
2.0
-
If the assignment does not change the value of the variable, the message is not displayed.
julia> const z = 100
100
julia> z = 100
100
The last rule applies to immutable objects, even if the binding of the variable changes, for example:
julia> const s1 = "1"
"1"
julia> s2 = "1"
"1"
julia> pointer.([s1, s2], 1)
2-element Array{Ptr{UInt8},1}:
Ptr{UInt8} @0x00000000132c9638
Ptr{UInt8} @0x0000000013dd3d18
julia> s1 = s2
"1"
julia> pointer.([s1, s2], 1)
2-element Array{Ptr{UInt8},1}:
Ptr{UInt8} @0x0000000013dd3d18
Ptr{UInt8} @0x0000000013dd3d18
However, for mutable objects, the warning is displayed as usual.
julia> const a = [1]
1-element Vector{Int64}:
1
julia> a = [1]
WARNING: redefinition of constant a. This may fail, cause incorrect answers, or produce other errors.
1-element Vector{Int64}:
1
Please note: although it is sometimes possible to change the value of the const
variable, it is strongly discouraged to do so. This feature is intended solely for convenience when working interactively. Changing constants can lead to various problems or unexpected results. For example, if a method refers to a constant and has already been compiled before changing it, the old value may be used.
julia> const x = 1
1
julia> f() = x
f (generic function with 1 method)
julia> f()
1
julia> x = 2
WARNING: redefinition of constant x. This may fail, cause incorrect answers, or produce other errors.
2
julia> f()
1
Typed global variables
Compatibility: Julia 1.8
Support for typed global variables was added in Julia 1.8. |
Global bindings can be declared not only as having a constant value, but also as having a constant type. This can be done without assigning an actual value using the global x' syntax.::T
or when assigned as `x::T = 123'.
julia> x::Float64 = 2.718
2.718
julia> f() = x
f (generic function with 1 method)
julia> Base.return_types(f)
1-element Vector{Any}:
Float64
For any assignment of a value to a global variable, Julia first tries to convert it to the appropriate type using the function convert
:
julia> global y::Int
julia> y = 1.0
1.0
julia> y
1
julia> y = 3.14
ERROR: InexactError: Int64(3.14)
Stacktrace:
[...]
The type does not have to be specific, but annotations with abstract types usually do not provide much performance benefit.
After assigning a value to a global variable or setting its type, you cannot change the binding type.
julia> x = 1
1
julia> global x::Int
ERROR: cannot set type for global x. It already has a value or is already set to a different type.
Stacktrace:
[...]