Search code examples
scopejuliaglobal-variablesgloballocal

Are variables used in nested functions considered global?


This is a dumb question, so I apologise if so. This is for Julia, but I guess the question is not language specific.

There is advice in Julia that global variables should not be used in functions, but there is a case where I am not sure if a variable is global or local. I have a variable defined in a function, but is global for a nested function. For example, in the following,

a=2;
f(x)=a*x;

variable a is considered global. However, if we were to wrap this all in another function, would a still be considered global for f? For example,

function g(a)
  f(x)=a*x;
end

We don't use a as an input for f, so it's global in that sense, but its still only defined in the scope of g, so is local in that sense. I am not sure. Thank you.


Solution

  • You can check directly that what @DNF commented indeed is the case (i.e. that the variable a is captured in a closure).

    Here is the code:

    julia> function g(a)
             f(x)=a*x
           end
    g (generic function with 1 method)
    
    julia> v = g(2)
    (::var"#f#1"{Int64}) (generic function with 1 method)
    
    julia> dump(v)
    f (function of type var"#f#1"{Int64})
      a: Int64 2
    

    In this example your function g returns a function. I bind a v variable to the returned function to be able to inspect it.

    If you dump the value bound to the v variable you can see that the a variable is stored in the closure.

    A variable stored in a closure should not a problem for performance of your code. This is a typical pattern used e.g. when doing optimization of some function conditional on some parameter (captured in a closure).

    As you can see in this code:

    julia> @code_warntype v(10)
    MethodInstance for (::var"#f#1"{Int64})(::Int64)
      from (::var"#f#1")(x) in Main at REPL[1]:2
    Arguments
      #self#::var"#f#1"{Int64}
      x::Int64
    Body::Int64
    1 ─ %1 = Core.getfield(#self#, :a)::Int64
    │   %2 = (%1 * x)::Int64
    └──      return %2
    

    everything is type stable so such code is fast.

    There are some situations though in which boxing happens (they should be rare; they happen in cases when your function is so complex that the compiler is not able to prove that boxing is not needed; most of the time it happens if you assign value to the variable captured in a closure):

    julia> function foo()
               x::Int = 1
               return bar() = (x = 1; x)
           end
    foo (generic function with 1 method)
    
    julia> dump(foo())
    bar (function of type var"#bar#6")
      x: Core.Box
        contents: Int64 1
    
    julia> @code_warntype foo()()
    MethodInstance for (::var"#bar#1")()
      from (::var"#bar#1")() in Main at REPL[1]:3
    Arguments
      #self#::var"#bar#1"
    Locals
      x::Union{}
    Body::Int64
    1 ─ %1  = Core.getfield(#self#, :x)::Core.Box
    │   %2  = Base.convert(Main.Int, 1)::Core.Const(1)
    │   %3  = Core.typeassert(%2, Main.Int)::Core.Const(1)
    │         Core.setfield!(%1, :contents, %3)
    │   %5  = Core.getfield(#self#, :x)::Core.Box
    │   %6  = Core.isdefined(%5, :contents)::Bool
    └──       goto #3 if not %6
    2 ─       goto #4
    3 ─       Core.NewvarNode(:(x))
    └──       x
    4 ┄ %11 = Core.getfield(%5, :contents)::Any
    │   %12 = Core.typeassert(%11, Main.Int)::Int64
    └──       return %12