Even after quite some time, I still struggle with understanding how to create type-stable wrappers around simple functions. I’ve read the docs, I know that closures in julia use reference semantics instead of value semantics (and that Jeff regrets this choice), but I just can’t wrap my head around it.
A couple of silly examples to try and illustrate my confusion. It makes intuitive sense that the type of a global variable may change under the compiler’s nose, so we have to set it as a constant to have a type-stable closure:
f(x,y) = x+y
@code_warntype f(2,2) #Type-stable
k = 2
f_k(x) = f(x,k)
@code_warntype f_k(2) #Not type-stable
const k_const = k
f_k_const(x) = f(x,k_const)
@code_warntype f_k_const(2)#Type-stable
However, sometimes this does not seem to be enough:
using Lux, Random
C = Dense(4=>4,relu) #Simple neural network, with parameters and states
ps,st = Lux.setup(Random.default_rng(),C)
x = rand(4)
@code_warntype C(x,ps,st) #Type-stable
f(x,p) = C(x,p,st) #Don't want to think about states, give it a default value
@code_warntype f(x,ps) #Type-unstable
const st_const = st #Maybe if we make st a const, it will be type-stable?
g(x,p) = C(x,p,st_const)
@code_warntype g(x,ps) #No :(
The only way I’m able to make this work is with strong scoping by means of let
statements:
i = let C = C, st = st
(x,p) -> C(x,p,st)
end
@code_warntype i(x,ps)# Type-stable
This is a silly amount of code for such a simple task, and it’s quite burdensome to write this down every time I want to build a closure.
Can anyone share a mental model that explains how these things work? It’s been hard for me to “think like a compiler”.
As far as I can tell, the let statements kind of enforce value semantics by means of copies, which is what I want basically 100% of the time. Can’t I achieve the same thing without the boilerplate?
Any insights would be highly appreciated.