Behaviors of `deepcopy` for a function constructed with different ways of referencing a global variable

This discovery stemmed from the reply in another post, but since it’s less relevant to the initial post, I thought I would make a new post dedicated to it.

julia> box = fill(1)
0-dimensional Array{Int64, 0}:
1

julia> function f1()
           function (x)
               box[] + 1
           end
       end
f1 (generic function with 1 method)

julia> function f2(localBox::Array{Int, 0})
           function (x)
               localBox[] + 1
           end
       end
f2 (generic function with 1 method)

julia> f1c = f1()
#1 (generic function with 1 method)

julia> f2c = f2(box)
#3 (generic function with 1 method)

julia> box[] += 1
2

julia> f1c(1)
3

julia> f2c(1)
3

julia> f1c_dc = deepcopy(f1c)
#1 (generic function with 1 method)

julia> f2c_dc = deepcopy(f2c)
#3 (generic function with 1 method)

julia> box[] += 1
3


julia> f2c_dc(1)
3

julia> f1c_dc(1)
4

julia> check_type(::F) where {F} = (isbitstype(F), Base.issingletontype(F))
check_type (generic function with 1 method)

julia> check_type(f2c)
(false, false)

julia> check_type(f1c)
(true, true)

Why didn’t deepcopy sever the reference to the global variable box for f1c_dc? And why is f1c considered an instance of bitstype and singletontype even though it technically references the “same” global variable box as f2c? I understand that for f2c, the box was never directly used to construct its body, but through a local variable localBox that points to box. However, the result remains effectively the same for f1c and f2c, as they both reference the same data stored in box.

Consider the case where an object is not a closure but a Tuple:

julia> v1 = (box,)
(fill(3),)

julia> v2 = deepcopy(v1)
(fill(3),)

julia> box[] = 5
5

julia> v1
(fill(5),)

julia> v2
(fill(3),)

Even though v1 is constructed by directly using box, applying deepcopy on v1 sever its internal data’s connection to box (see v2). Why does deepcopy not affect f1c the same way it affects f2c?

Flip the question on its head, why can’t the closure f2c have a singleton type? Hint: methods of closures are NOT redefined when the outer function is called.

Thanks for the hint!! It seems like the way I thought of Function, as struct with attributes (fields) and methods, is no longer applicable in this case. Here’s what I understand now:

For f1, box was not an assigned field of the closure it returns, but simply a variable directly inside the definition (function body) of the closure, except it’s declared outside the closure scope. So, as soon as f1 is defined, even before it is called, it is certain that there can only be one instance of the closure (which technically should not even be considered a “closure” based on your and @danielwe 's comments (1, 2) on the other post) that f1 generates. Thus, typeof(f1c) is deemed a singleton type. And because typeof(f1c) is a singleton, it should have one and only one instance (characterized by its type information), which is f1c. So deepcopy cannot just create another instance that is not identical to f1c. Therefore, deepcopy cannot sever the f1c’s reference to the global variable box either.

However, for f2, there can be many instances of the closure it generates, depending on the specific value of localBbox passed in. So f2 in fact behaves like a struct with a single field (store the value of localBbox for each instance) and a corresponding method that uses the field. Hence, f2c is not an instance of a singleton type. Correspondingly, deepcopy also copies the data stored inside it.

Hopefully, what I described above is a more accurate mind model of how closures and functions with out-of-scope variables generally work. Thanks!!

1 Like