Defining a function as
function f()
...
end
does not follow the same rules as variable assignment f = ...
. Your first example, without surrogate
works differently with variable assignment, even though the f
here is is the same within the loop and outside. (In the surrogate
case, the f
is boxed, as it should be).
function test()
v = Function[]
for i = 1:3
f = () -> println(i)
push!(v, f)
end
map(x -> x(), v)
f = () -> println("single-out")
end
test(); # 1 2 3
As discussed in the github-thread Benny links to, defining a named function f()
involves creating a method table for the function to use for storing different methods of the function, and this is somewhat complicated to handle in the general case. There are some unresolved issues with such definitions.
Anyway, we can have a look at what happens inside your first example, the non-surrogate:
julia> @code_warntype test()
MethodInstance for test()
from test() @ Main REPL[18]:1
Arguments
#self#::Core.Const(Main.test)
Locals
#8::var"#test##19#test##20"
@_3::Union{Nothing, Tuple{Int64, Int64}}
f::var"#f#test##18"
v::Vector{Function}
i::Core.Box
Body::var"#f#test##18"
1 ─ Core.NewvarNode(:(#8))
│ Core.NewvarNode(:(f))
│ %3 = Main.Function::Core.Const(Function)
│ (v = Base.getindex(%3))
│ %5 = Main.:(:)::Core.Const(Colon())
│ %6 = (%5)(1, 3)::Core.Const(1:3)
│ (@_3 = Base.iterate(%6))
│ %8 = @_3::Core.Const((1, 1))
│ %9 = (%8 === nothing)::Core.Const(false)
│ %10 = Base.not_int(%9)::Core.Const(true)
└── goto #4 if not %10
2 ┄ (i = Core.Box())
│ %13 = @_3::Tuple{Int64, Int64}
│ %14 = Core.getfield(%13, 1)::Int64
│ %15 = i::Core.Box
│ Core.setfield!(%15, :contents, %14)
│ %17 = Core.getfield(%13, 2)::Int64
│ %18 = Main.:(var"#f#test##18")::Core.Const(var"#f#test##18")
│ %19 = i::Core.Box
│ (f = %new(%18, %19))
│ %21 = Main.push!::Core.Const(push!)
│ %22 = v::Vector{Function}
│ %23 = f::var"#f#test##18"
│ (%21)(%22, %23)
│ (@_3 = Base.iterate(%6, %17))
│ %26 = @_3::Union{Nothing, Tuple{Int64, Int64}}
│ %27 = (%26 === nothing)::Bool
│ %28 = Base.not_int(%27)::Bool
└── goto #4 if not %28
3 ─ goto #2
4 ┄ %31 = Main.map::Core.Const(map)
│ %32 = Main.:(var"#test##19#test##20")::Core.Const(var"#test##19#test##20")
│ (#8 = %new(%32))
│ %34 = #8::Core.Const(var"#test##19#test##20"())
│ %35 = v::Vector{Function}
│ (%31)(%34, %35)
│ %37 = f::var"#f#test##18"
└── return %37
In block 2, we can see what’s happening inside the loop. The i
(which actually is boxed) is iterated over. Then at %18
and %19
an object of a struct called var"#f#test##18"
is created, with one field, the boxed i
. This is how capture of variables work. It’s a callable struct. Then, in %23
, the f
is pushed. We see it’s of type var"#f#test##18"
.
At the end of block 4, in %37
this type appears again, the same weirdly named type is returned from test
.
Now, what does this mean? All the f
s, those pushed, and the one outside the loop, are of the same type, a struct with one field:
julia> dump(var"#f#test##18")
struct var"#f#test##18" <: Function
i::Core.Box
What we can’t see is that this is a callable struct, the method table is not global, but if we insert a methods(f)
at the end of the test
-function, we see that it has a single method:
# 1 method for callable object:
[1] (::var"#f#test##18")()
Now, let’s call this struct F
, to avoid the clutter. It looks like this:
struct F <: Function
i::Core.Box
end
It has a method, a single method, defined like:
(fs::F)() = something
So, no matter what the i
is, the exact same something
is called. The question is what it is. There are two candidates for something
:
(fs::F)() = println(fs.i)
or
(fs::F)() = println("single-out")
However, this is not to be found inside the lowered test
function, we can’t see it. It’s not part of the execution of test()
, it has been defined once together with the struct, not repeatedly inside the loop or elsewhere. As Jeff says in the github-thread, it would be too slow to serve any practical purpose. We know it’s the last alternative, that’s what’s printed.
In the surrogate
case, something else happens. By inserting a @code_warntype surrogate(1)
in test()
, we see that the f
inside surrogate
and the one outside, are different. So there are two callable structs, one inside surrogate
which prints the i
, and another for the f
outside, which prints “single-out”. And, inside that test
, the f
is boxed to allow changing the type.