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/forscoping 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
structcomponent 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 astructinstance, 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=rin the Performance Tips section creates a new localrin 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. Theletblock is assigned tofbecause 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;forandwhiledo not. -
Case 3 and case 4 in the OP are a couple known workarounds to get type inference working. Case 3: global
structandfunctiondefinitions are implicitlyconstvariables (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.