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!