Some more thoughts for OP because I’m taking OP’s word that they are new to Julia, though I have to say this is not a beginner’s question.
-
The scoping rules mentioned earlier reminded me to say that closures capture by variable rather than by value. It’s not worded this way but the behavior is shown in the “Let Blocks” and “Loops and Comprehensions” sections of Scope of Variables · The Julia Language. Note how different closures capturing the same variable will use that variable’s most updated value, not the value at capture; to replicate the behavior of capturing values, you have to create different variables, and Julia’s
let
/for
scoping rules makes that easy. -
As mentioned and linked by @bkamins, a closure that captures variables is implemented as a hidden functor. The fields of the
struct
component correspond to the captured variables. Given this implementation, it actually makes a little sense that inference only happens when the captured variable is assigned just once prior to the closure. The closure is just astruct
instance, and you can’t reassign a field there. If the captured variable is assigned after the closure or is assigned multiple times, then when the closure is instantiated, the corresponding field must be a mutable placeholder, the Core.Box. Core.Box has been described as an internal analog ofBase.RefValue{Any}
, so hypothetically it could be redesigned to be parametric like aRefValue
, which I expect a fix of issue 15276 to do. -
The workaround
let r=r
in the Performance Tips section creates a new localr
in a new local scope and assigns the outerr
’s value to it, and boxing does not happen because that assignment occurs only once and prior to the closure instantiation. Thelet
block is assigned tof
because the block expression’s value is the last line’s value, so it somewhat acts like a function’s return. Note that not all block expressions do this;for
andwhile
do not. -
Case 3 and case 4 in the OP are a couple known workarounds to get type inference working. Case 3: global
struct
andfunction
definitions are implicitlyconst
variables (local variables cannot beconst
!), and since functions only check global variables when compiled, they can compile with known constants, which are great for type inference. Case 4: functions compile with argument instances present at the function call, so passing in a function won’t be a problem for type inference. You should be careful that input variables are type stable; while the function call itself might be type-stable, you would have to dispatch to one of many functions at runtime rather than dispatching to one known function at compile-time of the surrounding method. Functions are singletons (a type with only 1 instance), so you should make sure the input variable is only ever assigned that one function, even if multiple times for no reason e.g.ff4 = ff4
.