Is this a bug? Nested Functions and NLsolve

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.

  1. 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.

  2. 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 a struct 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 of Base.RefValue{Any}, so hypothetically it could be redesigned to be parametric like a RefValue, which I expect a fix of issue 15276 to do.

  3. The workaround let r=r in the Performance Tips section creates a new local r in a new local scope and assigns the outer r’s value to it, and boxing does not happen because that assignment occurs only once and prior to the closure instantiation. The let block is assigned to f 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 and while do not.

  4. Case 3 and case 4 in the OP are a couple known workarounds to get type inference working. Case 3: global struct and function definitions are implicitly const variables (local variables cannot be const!), 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.

1 Like