Scoping in nested functions - Why am I not getting an error?

I am confused about the behavior of scoping when using a nested function. I am using Julia 1.4.1.

MWE:

function main()

    function doit()
        A = ones(Int, 2) # <-- I expected this to introduce a new A within the scope of doit
        return A
    end

    doit()
    display(A) # <-- Why is this not an error? I haven't defined A yet
    A = [1, 2, 3] # <-- Now A is defined
    display(A)
    doit()
    display(A) # <-- Why is A overwritten? I didn't assign it to the output of doit()

end

Output:

julia> main()
2-element Array{Int64,1}: # Expected an error for undefined A
 1
 1
3-element Array{Int64,1}: # This is what I expected
 1
 2
 3
2-element Array{Int64,1}: # Expected [1, 2, 3] again
 1
 1

It looks like there is no new scope introduced for doit. Is that correct? Can someone explain why that is, or if I am misunderstanding something?

My understanding is that when Julia compiles main() it notices that A gets defined in main(). Therefor when the child function set A it’s actually setting A in the parent context. The compiler doesn’t care when A is set just that it was set.

One way to avoid this situation is that when defining the variable prefix it with local that will ensure that the variable is, well, local. So if you did local A = ones(Int, 2) then that A would be local to doit() and you would get the expected error.

This is documented in the manual:
https://docs.julialang.org/en/v1/manual/variables-and-scoping/#Local-Scope-1

Note that nested functions can modify their parent scope’s local variables:

julia> x, y = 1, 2;

julia> function baz()
          x = 2 # introduces a new local
          function bar()
              x = 10       # modifies the parent's x
              return x + y # y is global
          end
          return bar() + x # 12 + 10 (x is modified in call of bar())
      end;

julia> baz()
22

julia> x, y # verify that global x and y are unchanged
(1, 2)

The reason to allow modifying local variables of parent scopes in nested functions is to allow constructing closures which have private state

In your example, the function is defined before the variable that it modifies, but the compiler doesn’t care of that — just as in the case of variables defined in the global scope.

3 Likes

Yes, but in the manual x is defined before the nested function is defined. In my example, A was defined after the definition of the nested function. I understand why the example in the manual works as it does because x was created before the nested function was. I don’t understand why the nested function in my example captures a variable I haven’t defined yet.

EDIT: Sorry, I responded thinking I had already scrolled to the end of your post, so I missed

In your example, the function is defined before the variable that it modifies, but the compiler doesn’t care of that — just as in the case of variables defined in the global scope.

That’s a good point.

Also note that one doesn’t have to assign a value to the variable in the parent function, one can just declare it via local:

function baz()
    local x
    function bar()
        x = 2
    end
    bar()
    x
end

baz() # returns 2

I just noticed that this isn’t type stable.

function baz()
    local x
    function bar()
        x = 2
    end
    bar()
    x
end

baz() # returns 2

image

Is there a better way of doing this?

This does not address exactly what you are asking, but I think that having a closure that modifies the closed over variables is always walking on a danger zone. I would advice to simply don’t do it, and use patterns like:

julia> function baz()
           a = 5
           function bar()
               x = 2 + a # do not modify a here
           end
           y = bar()
           y
       end
baz (generic function with 1 method)

julia> @code_warntype baz()
Variables
  #self#::Core.Const(baz)
  y::Int64
  bar::var"#bar#5"{Int64}
  a::Int64

Body::Int64
1 ─      (a = 5)
│   %2 = Main.:(var"#bar#5")::Core.Const(var"#bar#5")
│   %3 = Core.typeof(a::Core.Const(5))::Core.Const(Int64)
│   %4 = Core.apply_type(%2, %3)::Core.Const(var"#bar#5"{Int64})
│        (bar = %new(%4, a::Core.Const(5)))
│        (y = (bar::Core.Const(var"#bar#5"{Int64}(5)))())
└──      return y::Core.Const(7)