As you probably have tried, it works in the interactive REPL and notebooks, but not in files or in Julia expressions. Yes this is a very weird discrepancy with a weird history, it’s intuitive to nobody.
Minimal example in the REPL
julia> x = 0
0
julia> for i in 1:10
x += i
end
julia> x
55
julia> @eval begin
x = 0
for i in 1:10
x += i
end
x
end
┌ Warning: Assignment to `x` in soft scope is ambiguous because a global variable by the same name exists: `x` will be treated as a new local. Disambiguate by using `local x` to suppress this warning or `global x` to assign to the existing global variable.
└ @ REPL[4]:4
ERROR: UndefVarError: `x` not defined in local scope
First the principles. You need to declare global variables, like global num
in another (even subsequent!) line or global num += 1
in the same line, in local scopes. All blocks except begin
and if
introduce a local scope for convenient variable lifetimes and capturing, so you’ll have to write global
often if you work with global variables. Alternatively, you could put the whole thing in a let
block so it’s all local variables.
The reason for a local scope freely borrowing from outer local scopes but not the global scope is because only the global scope can be scattered across several include
-d files, so this disambiguation protects you from accidentally reassigning a global variable from 15 files ago when you meant to write a local variable. The soft scope concept relaxes this for for
, while
, try
, and struct
blocks for convenient use in the REPL and notebooks, especially pasting local scope code. (I’m actually not sure why struct
was included because struct
blocks don’t run code outside its inner constructors in practice.)
Described fixes for the minimal example earlier
julia> @eval begin
x = 0
for i in 1:10
global x += i
end
x
end
55
julia> @eval let
x = 0
for i in 1:10
x += i
end
x
end
55
Now the history. The relaxed behavior was around first, but people complained it was hard to remember and tell which blocks would make an assignment affect a global variable, especially from another file. So, the stricter behavior was done for all local scope blocks in v1.
Then people complained they needed to write global
everywhere and remove it for pasting into local scopes. While some new users from other languages were just complaining this isn’t what they’re used to, there was an annoying factor that affected anyone: executing code by line is routinely done for debugging and development, and running lines in the globally scoped REPL had to be done when the debuggers that could step through local scopes weren’t around yet. IJulia (for Jupyter) independently reverted to the pre-v1 behavior, so v1.5 introduced the soft scope concept as a compromise to divide the relaxed and strict behaviors for where they were demanded the most.
I personally dislike this discrepancy and would prefer the strict v1 behavior for safety, but since v1 had to exist first for debuggers to even start development, I don’t know how it should have turned out differently. I also definitely wouldn’t want more blocks to not introduce local scopes; that’s a few more major gripes in other languages I’m glad to not to deal with.