A variable is a naming symbol used as a placeholder or label for a value. When we declare a variable, we claim a symbol to use as the name for the variable. When we assign a value to a variable, we associate the value with the variable so that we may retrieve the value by referring to the variable’s symbol.
We can declare a variable x in a few explicit or implicit ways (type annotations are optional):
explicit:
local x::Int64global x::Int64
implicit:
let x::Int64 endf(x::Int64) = nothingfunction f(x::Int64) = nothing endmap([]) do x::Int64 nothing endfor x in 0:1(x for x in 0:1)[x for x in 0:1]function x endstruct x endx, y = (0, 1)(; x, y) = (x = 0, y = 1)f((x, y)) = x + y
We say a variable is defined when we have declared the variable and assigned a value to it, potentially in a single step. That means a variable is not defined if it is not declared or declared but not assigned to.
module M
@assert !(@isdefined x) # `x` is not defined (it is not declared)
global x # declaring `x` as a global variable
@assert !(@isdefined x) # `x` is declared but not assigned to, so not defined
x = 1 # assigning to `x`
@assert (@isdefined x) # `x` is defined
end
In Julia, the expression x = 0 can mean one of two things, depending on the context:
- “declare a variable
xand assign to it the value0” (definition. implicit declaration and assignment) or - “assign the value
0to the variablexthat has already been declared” (assignment)
The context that determines which of these meanings applies depends on the scope in which the expression x = 0 gets evaluated. (In some cases, it also depends on whether the expression is evaluated in an interactive environment such as in the REPL as opposed to in a script environment such as in a file. For now, ignore evaluation in interactive environments and consider only evaluation in script environments.)
Each variable has a scope, which is the section of code where the value of the variable can be retrieved or where the variable can (potentially) be assigned a value. A variable cannot be accessed outside its scope; so outside a variable’s scope, we can use the variable’s symbol as a name for another variable. Julia has two kinds of scope: local and global.
The following constructs in Julia define the boundaries of local scope:
- struct-end
- function-end
- do-end
- let-end
- for-end
- while-end
- try-end
- comprehensions
- generators
Let's call these the locally-scoped blocks.
The following constructs define the boundaries of global scope:
- module-end
- baremodule-end
Let's call these the globally-scoped blocks.
If a variable is not declared in a locally-scoped block, its declaration occurs implicitly or explicitly in a globally-scoped block.
A variable that is declared in a locally-scoped block is called a local variable, or simply a local (unless it is explicitly declared global). A variable that is declared in a globally-scoped block is called a global variable, or simply a global (unless it is explicitly declared local).
A locally-scoped block may contain other locally-scoped blocks but not globally-scoped blocks. If a locally-scoped block contains another locally-scoped block, all variables accessible from the outer block are also accessible from the inner one, but local variables declared in the inner block are not accessible from the outer one.
module M
let # outer locally-scoped block
let # middle locally-scoped block
x = 0 # `x` is implicitly declared and assigned to
let # inner locally-scoped block
@assert (@isdefined x) # `x` is accessible here
end
end
@assert !(@isdefined x) # `x` is not accessible here
end
end
A globally-scoped block may contain locally-scoped or globally-scoped blocks. If a globally-scoped block contains another block, all variables accessible from the outer block are also accessible from the inner one if the inner one is locally-scoped, but none of them are accessible from the inner one if the inner one is globally-scoped.
module Outer # outer globally-scoped block
x = 1 # `x` is implicitly declared and assigned to
module Inner # inner globally-scoped block
@assert !(@isdefined x) # `x` is not acessible
end
let # inner locally-scoped block
@assert (@isdefined x) # `x` is accessible
end
end
Inside a locally-scoped block, if a variable x is a declared local variable, and the expression x = 0 is in the scope of x (the variable is accessible to the expression), the expression assigns 0 to that variable; otherwise, it declares and assigns to (defines) a new local variable x.
module M
let
x = 0
let
x = 1 # assigns to an already-declared variable `x`
end
@assert x == 1 # `x` was assigned to from an inner block
end
let
let
x = 1 # declares the local variable `x` and assigns to it
end
@assert !(@isdefined x) # `x` is not defined
end
let
let
x = 1 # assignment to a variable `x` that is declared later in the code
end
@assert (@isdefined x) # `x` is defined
@assert x == 1 # `x` is defined
local x # variable declaration; appears later in the code than the assignment
end
let
let
x = 1 # assignment to a variable `x` that is declared later in the code
end
@assert (@isdefined x) # `x` is defined
@assert x == 1 # `x` is defined
x = 0 # implicit declaration; appears later in the code than the assignment
end
function f()
function inner()
@assert !(@isdefined x) # `x` is not defined
return nothing
end
return inner()
end
f()
function g()
function inner()
@assert (@isdefined x) # `x` is defined
return nothing
end
x = 0 # `x` is implicitly declared and assigned to outside of `inner`
return inner()
end
g()
let x = 0 # `x` is declared and assigned to outside of `inner`
function inner()
@assert (@isdefined x) # `x` is defined
return nothing
end
inner()
end
let
function inner()
@assert (@isdefined x) # `x` is defined
return nothing
end
x = 0 # `x` is implicitly declared and assigned to outside of `inner`
inner()
end
f(y) = true
let x = 0
function f(y)
x = y ^ 2 # assigns to the already-declared `x`
return x
end
sort(99:100; by=f) # it is not necessary that the last call of `f` is on `100`
@assert x == 99 ^ 2
end
@assert f(0)
end
Variables declared as local in globally-scoped blocks can be accessed only in the expression where they are declared.
module M
local x = 0
@assert !(@isdefined x)
begin
local x = 0
@assert (@isdefined x)
end
@assert !(@isdefined x)
local x; x = 0; @assert (@isdefined x)
@assert !(@isdefined x)
end
An explicit global keyword is required to assign to a declared global variable from locally-scoped blocks.
module M1
x = 0
let
x = 1
end
@assert x == 0
end
module M2
x = 0
let
global x = 1
end
@assert x == 1
end
module M3
function outer()
global x = 0
function inner()
x = 1 # this `x` is global
end
inner()
return x
end
outer()
@assert x == 1
end
struct or function blocks are evaluated in global scope even if they are defined inside local blocks. That is, structs and functions defined in local blocks are globals.
module M
let
struct S
end
end
@assert (@isdefined S)
S()
end
The following blocks are neither locally-scoped or globally-scoped:
- if-end
- begin-end
So whether a variable is declared inside these blocks does not determine the variable's scope. To determine a variable's scope in such cases, consider instead whether these blocks are themselves contained in locally-scoped or globally-scoped blocks.
Variables captured in closures can be assigned (or reassigned) values after a closure is defined:
module M
f = Ref{Any}()
let x
f[] = ()-> x
x = 1
end
@assert f[]() == 1
counter = Ref{Any}()
let i = 0
counter[] = ()-> i += 1; i
end
@assert counter[]() == 1
@assert counter[]() == 2
@assert counter[]() == 3
offset = 100
offset_counter = Ref{Any}()
let i = 0
offset_counter[] = ()-> i += 1; i
i = i + offset
end
@assert offset_counter[]() == 1 + offset
@assert offset_counter[]() == 2 + offset
@assert offset_counter[]() == 3 + offset
end
In for-end blocks, each iteration declares a new local variable:
module M
for i in 0:1
@assert !(@isdefined x) # the `x` defined in one loop is not defined in subsequent loops
x = 1
end
fs = Vector{Any}(undef, 2)
for i in 1:2
fs[i] = ()-> i
end
@assert fs[1]() == 1
@assert fs[2]() == 2
gs = Vector{Any}(undef, 2)
for i in 1:2
gs[i] = ()-> i
i += 1
end
@assert gs[1]() == 2
@assert gs[2]() == 3
end
Let's call the following locally-scoped blocks softly-scoped blocks (and the remaining, hardly-scoped):
for-endtry-endwhile-endstruct-end
Say x is a declared global variable in the REPL (Main module). By default, the expression x = 0 is evaluated as if x were a local variable if it appears in the softly-scoped blocks, which may themselves occur recrsively inside a globally-scoped or softly-scoped block. The variable x, in such cases, is said to have soft scope within the block where the x = 0 expression occurs.
global x
for i in 1:1
x = 0
end
@assert x == 0
for i in 1:1
for j in 1:1
x = 1
end
end
@assert x == 1
global x
for i in 1:1
if true
x = 0
end
end
@assert x == 0
struct A
if true
x = 1
end
end
@assert x == 1
You can turn off soft scope in the REPL by executing the following, at least as of Julia version 1.10.3:
empty!(Base.active_repl_backend.ast_transforms)