Is it possible to know if a function has captured local variables?

It’s a subtle distinction at first, but a closure is a narrower concept than simply a function referencing a variable from a parent scope. A closure is a function that does this after the parent scope has expired, and therefore needs to carry the variables with it as part of the function object.

This is not a closure:

a = Ref(1)
f(x) = x + a[]

This function doesn’t package anything extra, it’s just a regular function that happens to reference a global variable. Every time this function is called, it will dynamically look up the current value of a in the global scope, according to the normal rules of lexical scoping.

This is a closure:

g = let b = Ref(1)
    x -> x + b[]
end

Here, the lifetime of the variable b will have expired by the time g is assigned, since it was defined in the local scope started by let and ended by end. Thus, it’s up to g to keep b alive such that future calls still work. Therefore, a reference to b is stored as part of the g object. This is what a closure is: a function that comes packaged with one or more bindings from an expired scope. It’s actually just stored as a field. Try it yourself: g.b[] == 1. (This is an implementation detail, though, you shouldn’t rely on it.)

In contrast, there is no f.a. It’s not needed, since a is still alive and well in the global scope, such that f can reference it at any time.

This explains your observations. f2 = deepcopy(f) just gives you a function identical to f. In fact, since functions are immutable singletons, it will return f itself, such that f2 === f. The variable a is not involved, since there’s no reference from the object f to a.

In contrast, g2 = deepcopy(g) will recursively walk the g object, notice the reference to b, and make a copy of b and assemble a new function of the same type as g using the new copy of b. Thus, g.b and g2.b are not the same object, so mutating g.b does not change the behavior of g2, and vice versa.

Hope this helps!

6 Likes