Local scope rules are confusing in inner scopes (Julia 1.6)

I am using Julia 1.6.1:

julia> VERSION
v"1.6.1-pre.1"

I find the local scope rules are confusing to apply to inner scopes (such as inner functions). The rules are described at described at Scope of Variables · The Julia Language and copied below:

  1. Existing local: If x is already a local variable , then the existing local x is assigned;
  2. Hard scope: If x is not already a local variable and assignment occurs inside of any hard scope construct (i.e. within a let block, function or macro body, comprehension, or generator), a new local named x is created in the scope of the assignment;
  3. Soft scope: If x is not already a local variable and all of the scope constructs containing the assignment are soft scopes (loops, try / catch blocks, or struct blocks), the behavior depends on whether the global variable x is defined:
  • a. if global x is undefined , a new local named x is created in the scope of the assignment;
  • b. if global x is defined , the assignment is considered ambiguous:
    • i. in non-interactive contexts (files, eval), an ambiguity warning is printed and a new local is created;
    • ii. in interactive contexts (REPL, notebooks), the global variable x is assigned.

Now, consider this example:

function funout()
    global x = 1

    function funin()
        x = 2
    end

    funin()

    return x  # x is global variable
end

Here, the inner function funin() creates a hard scope. The assignment x = 1 before the definition of funin() is global (not local), so I thought the assignment x = 2 inside funin() would create a new local variable according to Rule 2 above. However, executing funout() actually updates the global variable:

julia> funout()
2

julia> x  # global variable
2

Consider another example:

function myfun()
    global x = 1

    for i = 1:3
        x = i
    end

    return x
end

Here, the for loop inside the function myfun() creates a soft scope. Again, the assignment x = 1 before the for loop is global (not local). Therefore, the above definition of myfun() is put in a file and and executed, I thought Rule 3.b.i would apply. However, neither an ambiguity warning is generated, nor the assignment x = i in the for loop creates a new local variable:

julia> code = """function myfun()
           global x = 1

           for i = 1:3
               x = i
           end

           return x
       end

       myfun()
       """;

julia> include_string(Main, code)
3

julia> x  # global variable
3

Based on these examples, I feel that something is missing from the description of the local scope rules in the documentation. Are there any exceptions to the rules related to the inner scopes or the usage of the global keyword?

UPDATE. I realized that Rule 3 applies when all the scopes enclosing the assignment are soft scopes. So Rule 3.b.i does not apply to the second example because myfun() is a hard scope. But then, the local scope rules don’t seem to specify what happens to the assignment inside a soft scope enclosed by a hard scope.

1 Like

Yes, I think the explanation in the manual — despite quite a bit of revising over the years — is still not ideal. Overall, I think too much has been made of the hard- vs. soft-scope distinction. The way I think about it is that once you are inside a scope that can have local variables (e.g. let, for, or a function), everything follows very simple lexical scope rules: all variables and declarations are inherited by inner scopes. Things are only weird in the very first transition from global to local scope. That’s the only place “hardness” matters: for some forms (e.g. functions), we’ll always introduce new locals by default, and for others “it depends”. But once inside the comfort of a local scope, you don’t need to know which scopes are “hard”; they all behave the same — or at least they should!! Any exceptions will be entertained as bug reports.

4 Likes