Closures: pass by value or reference?

Is the following true?

“When copied inside a function, variables become local and functions become closures.” (unknown)

What causes then the different behavior below?

solver(f,x) = f(x)
a, b, c = 1, 2, 3;
g(x) = a*x^2 + b*x + c
x = 5.;
@show solver(g,x)    # 38.0
a = 10
@show solver(g,x)    # 263.0 (tracking a)

add(y) = x -> x + y
a = 5
addFive = add(a) 
@show addFive(3)     # 8 
a = 4 
@show addFive(3)     # 8 (not tracking a)

The function g (or solver) isn’t a closure. The global variables a, b, and c are looked up dynamically (in your language: they get “tracked”). Note the Main.a, Main.b, and Main.c in the lowered code that indicates this:

julia> @code_lowered g(x)
CodeInfo(
1 ─ %1 = Core.apply_type(Base.Val, 2)
│   %2 = (%1)()
│   %3 = Base.literal_pow(Main.:^, x, %2)
│   %4 = Main.a * %3
│   %5 = Main.b * x
│   %6 = %4 + %5 + Main.c
└──      return %6
)

The function addFive is a closure. It is the value of a that gets stored inside of the function (rather than a dynamic lookup). Note that, according to my understanting of the concept of a closure, a crucial property is that the closed-upon variable is actually wrapped. For addFive we can actually access y:

julia> propertynames(addFive)
(:y,)

julia> addFive.y
5

I’d say the statement, as you stated / cited it, is partially incorrect and partially correct. When you copy the definition of a, b, and c into the definition of g, i.e.

function g(x)
    a, b, c = 1, 2, 3
    a*x^2 + b*x + c
end

the variables a, b, and c indeed become local variables. However, I wouldn’t call g a closure in this case as it doesn’t wrap a, b, c, in the sense of “private variables”.

julia> function g(x)
           a, b, c = 1, 2, 3
           a*x^2 + b*x + c
       end
g (generic function with 1 method)

julia> propertynames(g)
()

In fact, generally speaking, the compiler might optimize all local variables away if it can prove that this doesn’t have an observable effect.

But perhaps one may call this a closure nonetheless? Someone wiser should speak to this :slight_smile:

5 Likes

I don’t think that the definition of a closure depends on that (does it?). For example:

julia> function f(x)
           a = [1]
           function g(x)
               x = a[1] + 2
               a[1] = 0
               return x
           end
           return a, g(x)
       end
f (generic function with 1 method)

julia> f(5.0)
([0], 3)

This is a bad pattern (modifying the values of the captured values). I think there is even an issue associated with performance problems associated to this with this name. Yeah, here it is: performance of captured variables in closures · Issue #15276 · JuliaLang/julia · GitHub

Yeah, it doesn’t. I was referring to the “cited” statement in the OP. I assumed that the examples try to test the validity of this statement. And the latter mentions copying.

Anyways I just dropped that sentence.

But you think that this g below is not a closure, by definition?

julia> a = 1
1

julia> g(x) = x + a
g (generic function with 1 method)

julia> g(2)
3

(I’m particularly worried because the OP took the example from my notes :- :grimacing:).

My understanding is these are closures anyway, which turn out to be inefficient because here they are capturing a non-constant global variable, but that is another story).

No, that pattern is fine. #15276 is about multiple assignments to a captured variable, not mutation.

2 Likes

Yes, sorry, that was dumb. I added the mutation to show that the value changed, but with the mutable variable the example is not what it should be. With immutable variables one cannot do the same, and the issue is on assignments.

Maybe this is my confusion. In the following example, from the above issue:

julia> f = let
           x::Int = 1
           () -> (x = x + 1.0; x)
       end
#4 (generic function with 1 method)

I understood that () -> (x = x + 1.0; x) was “the closure”. Is that a closure by definition? Or just a regular function, with no parameters, which happens use the value of x which is defined on a larges scope? I may have interpreted that that was a closure from the superficial reading of the comments, but clearly that is not the same pattern as h(a) = x -> x + a, in which the variable of the function and the parameter that is captured are explicit.

Or, alternatively, when one does:

julia> x = [1,2,3];

julia> a = 2
2

julia> findfirst(el -> el > a, x) 
3

Is el -> el > a a closure, in any sense? (thanks @CameronBieganek for the additional info below).

ps: @Bardo , I think now everything I speak about in my notes are “anonymous functions” only, not closures. I will rewrite that.

Closures can only be created in a local scope, so you have to either define a function inside a let block or inside another function. So, the above example is in fact a regular function (not a closure) that dynamically looks up the value of a global variable.

2 Likes

I don’t think so. Even though the anonymous function el -> el > a is passed to findfirst, it is still defined in the global scope and thus it cannot be a closure.

1 Like

I don’t know if this is part of the confusion, but a closure does not have to be an anonymous function, it can be a named function. Any function (anonymous or not) defined in a local scope will capture variables in the enclosing scope to which the function refers:

let
    a = 1
    global foo(x) = x + a
end
julia> foo(10)
11

So, foo is a closure in the above example.

2 Likes