1

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.

  • 1
    Yes, this is called a _closure_: https://en.wikipedia.org/wiki/Closure_(computer_programming) and we say that `f` "closes over the variable `a`". See e.g. https://stackoverflow.com/questions/30700027/what-does-it-mean-to-close-over-something – DNF May 31 '22 at 09:46

1 Answers1

1

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
Bogumił Kamiński
  • 66,844
  • 3
  • 80
  • 107