Confused about scope


#1

Hi,

module test
a = 1
function f()
    println(a)
end
end

module.f() prints 1, as expected.

module test
a = 1
function f()
    a = 2
    println(a)
end
end

This prints 2, but afterwards test.a is 1. I was expecting that a is a global variable seen from f() (consistent with the previous example), and therefore I can modify it. What am I understanding wrong here?

module test
a = 1
function f()
    println(a)
    a = 2
end
end

Errors because a is not defined! I guess this is because the compiler sees the “a = 2” after and therefore declares a as a local variable, but this is very surprising.


#2

It is surprising but that is the case.


#3

How else could it work, though? The scope of a can’t change within a block, so it can’t matter where a=2 happens within the block.


#4

Assignment inside a block* introduces a new variable, regardless of what variables are available outside. It’s very sensible if you think about it. Imagine, for example, that you write a function f() that uses a local variable a and later refactor your module to also have a global variable a. Should f() now work differently? I guess you would like f() to keep the same behavior because it’s totally unrelated to the global variable a.

I know very few cases for global variables, but if you really need them, try adding a global modifier to them in local scope. E.g. in:

module test
a = 1
function f()
    global a = 2
    println(a)
end
end

a will refer to the global variable instead of introducing a new local one.

* - there are several types of blocks in Julia, see docs for more information.


#5

What I’m confused about is the status of variables in modules, as seen inside functions. The first example seems to suggest they are global, but then why can’t I modify them in the second example? The third example is surprising (adding a definition of a later in the function makes it undefined!) but makes sense, I agree.


#6

In your second example, there are 2 different variables with the same name, one global and one local.


#7

dfdx: sorry, I was writing my message at the same time as you.

OK, the “an assignment introduces a local variable” semantics seems clear. Could a warning maybe be printed in such cases, though? It seems very error-prone.

Adding to my confusion was the fact that https://groups.google.com/d/msg/julia-users/FBTEhEJKOCo/VBDHYKKfdWkJ suggests that variables in an outer scope can be accessed and modified, but that the snipped shown there does not work (maybe it was for a previous version of julia?)

My use case is a simple package with numerical parameters being used in many functions. Instead of passing them around all the time, I figured it would be simpler to use them as globals. I could have a global “parameters” structure, but then I’d have to either refer to the parameters as eg params.N or unpack them at the beginning of the functions (I saw there’s a package for that). If there’s anything simpler I’m all ears.


#8

Variables are first searched in function bodys. If they are not defined in the function, only a global variable can be meant.
Whenever a function defines a local version of a, a only ever refers to that local variable.

Your first case falls back to global search and is indeed the exception.

Perhaps we could need something like Pythons LEGB rule


#9

Give Parameters.jl a try. You’ll notice it really isn’t much more code, and now all of your parameters are local variables.


#10

Again, this would change the behavior of the function without actually touching its code. The general approach in literally every (non-esoteric) programming language I know is to minimize the perception border of the code you read or write. One way to do it is to keep control flow local, i.e. write code in small independent chunks that you can reason about without looking at any other place. Imagine, that you read somebody’s large module and see a function:

function f()
    a = 2
    println(a)
end

What can you tell about a? With Julia’s scoping rules you definitely know that a is local, it doesn’t affect any global variables and this code doesn’t cause any warnings. From only 4 lines of code, you know exactly what will happen when you call this function. In addition, you can now introduce a global variable a somewhere in the module without worrying to affect any of local variables.

With global variables, it becomes much harder to reason about the code because in this case you have to keep in mind all the code, not just a little piece.

In your case I’d create a struct to keep all parameters together and pass it to all functions. Something like:

mutable struct Params 
    a::Int
    b::Float64
    c::String
end

function foo(p::Params, ...)
    ...
    p.a = 2
    println(p.a)
    ...
end

(use type instead of mutable struct if you are on Julia 0.5)

This way you need just a bit more keystrokes, but code maintainability increases drastically.


#11

I still like the simplicity of globals, but point taken. Thanks for the clarification!

Shouldn’t the following raise a warning at parse time, though? I suppose the user could mean a local variable but it seems more likely than not that it’s a bug.

module A a = 1 function f() a = 2 end end


#12

I’m partial to Golang’s subtle assignment vs declaration syntax.

:= declaration
= assignment


#13

My experience with Lint.jl suggests that the vast majority of the time this happens, it’s not a bug. Lint has a I341 message for this case (local variable shadowing global); it’s ignored by default because of how noisy it gets.

In practice, most modules do not often contain functions that modify global variables. There are a variety of reasons for this, not least being that non-const global variables are bad for performance. However, it is very common to use a local variable name identical to that of a global variable.