That for sure an interesting case
I think the perception of this depends a lot on what your previous programming experiences are. Coming from a C++ background, I’m more surprised that result1
works at all, simply since ff1
is used before being defined. But there are many languages which allow this kind of reference.
I’m not sure if I would call it a bug or a missing feature,
but maybe it helps to see how one could look at the situation.
A bit shorter is this example
function easy_capture()
y = [0]
get_y() = y
y[1] += 1
@code_warntype get_y()
return get_y()
end
easy_capture()
If we run this code the inside @code_warntype
prints
Arguments
#self#::var"#get_y#91"{Vector{Int64}}
Body::Vector{Int64}
1 ─ %1 = Core.getfield(#self#, :y)::Vector{Int64}
└── return %1
Which essentially says that get_y()
is a function which returns the data of type Vector{Int64}
which is defined in the local scope of the surrounding function.
Since the variable y
was already defined before get_y()
was defined, there is never the situation
that one would need to worry about calling get_y()
before the variable is defined.
To give an counter example of what could go wrong:
function bad_capture()
get_y() = y
@code_warntype get_y()
x = get_y() # Error: 'y' is not yet defined!
y = 10
return x
end
The problem is clearly that calling get_y()
too early is bad, which is reflected in the generated
code for this function
Arguments
#self#::var"#get_y#11"
Locals
y::Union{}
Body::Any
1 ─ %1 = Core.getfield(#self#, :y)::Core.Box
│ %2 = Core.isdefined(%1, :contents)::Bool
└── goto #3 if not %2
2 ─ goto #4
3 ─ Core.NewvarNode(:(y))
└── y
4 ┄ %7 = Core.getfield(%1, :contents)::Any
└── return %7
It says that it has to check if y
is defined (since it cannot predict when get_y()
will be called).
Now, let’s spice this up a little bit. You can even get type instability when you define y
before the capture!
function tricky_capture()
y = [0]
get_y() = y
@code_warntype get_y()
y = [1] # this create a new array and binds the variable `y` to this new data
return get_y()
end
tricky_capture()
Suddenly, get_y()
is more complicated since Julia cannot predict if the variable is bound to the array [0]
or the array [1]
or if it even exists!
Note that the line y = [1]
does not overwrite the existing array but creates a new array and bounds y
to this new array!
Looking at the code for the inner function we get
Arguments
#self#::var"#get_y#8"
Locals
y::Union{}
Body::Any
1 ─ %1 = Core.getfield(#self#, :y)::Core.Box
│ %2 = Core.isdefined(%1, :contents)::Bool
└── goto #3 if not %2
2 ─ goto #4
3 ─ Core.NewvarNode(:(y))
└── y
4 ┄ %7 = Core.getfield(%1, :contents)::Any
└── return %7
This is similar to before, whenget_y()
is called it checks if it is defined and when it returns it it cannot predict the type since the variables y
might have been bound to new data or not.
Overall: If you want type stability, make sure that whatever you want to capture is defined before
you define the function which should capture it (to avoid the bad_capture
case where you refer to undefined data)
and, in addition, make sure that the variable is never reassigned to new data. That means in place operations are fine (such as y[1] += 1
) but operations which bind y
to new data are problematic.