If I understand what you’re trying to do, it seems like the best approach is to set:
param::Float64 = 0.0
foo(x; β = param) = sum(x .* β)
and then the access to β will be type-stable because param has its type fixed. When users attempt to set param to an integer value, it automatically gets promoted to float:
julia> param = 2
param
2.0
The drawback to this is that the user won’t be able to adjust the precision of the global param, but with Float64 that’s often not necessary anyway.
To address what I detect seems to be a bit of confusion (correct me if I’m mis-estimating the situation!):
Even with the above type-stable definition, if the user writes this code in global scope:
x = (1,2,3)
foo(x)
the call to foo will be type-unstable anyway, because the type of global x is not constant, and runtime dispatch must occur.
It might seem like it’s type-stable, because @code_warntype says so,
julia> @code_warntype foo(x)
MethodInstance for foo(::Tuple{Int64, Int64, Int64})
from foo(x; β) @ Main REPL[1]:2
Arguments
#self#::Core.Const(foo)
x::Tuple{Int64, Int64, Int64}
Body::Float64
1 ─ %1 = Main.:(var"#foo#3")(Main.param, #self#, x)::Float64
└── return %1
but the @code_warntype macro isn’t telling the whole story. The macro doesn’t actually know whether x is const or not, or whether it has its type fixed or not; it just runs typeof(x) at that moment to find the concrete type of its value at that moment and runs with it. For a more accurate depiction that includes the effects of x’s type-instability, call @code_warntype (()->foo(x))() to wrap x in a function that captures it. Benchmarks are also educational.
Another note: notice from the above that a function var"#foo#3" has been declared behind the scenes, and is being called by foo(x). And notice that foo(x) passes param to it as an argument. Here’s another perspective:
julia> @code_lowered foo(x)
CodeInfo(
1 ─ %1 = Main.:(var"#foo#3")(Main.param, #self#, x)
└── return %1
)
(“Lowered” code is still Julia, just a subset of the language after the parser has transformed a bunch of syntax sugar into a lower-level form.)
Basically, foo(x) is actually:
foo(x) = var"#foo#3"(param, foo, x)
hence, the global param is simply captured and passed to a separate function:
julia> methods(var"#foo#3")
# 1 method for generic function "#foo#3" from Main:
[1] var"#foo#3"(β, ::typeof(foo), x)
@ REPL[1]:2
julia> @code_lowered var"#foo#3"(param, foo, x)
CodeInfo(
1 ─ nothing
│ %2 = Base.broadcasted(Main.:*, x, β)
│ %3 = Base.materialize(%2)
│ %4 = Main.sum(%3)
└── return %4
)
this is the function you actually wrote. Functions that take keywords are actually broken up into multiple functions behind-the-scenes. (try @code_lowered foo(x; β=params) too, and see if you can find var"#foo#3" (or whatever it’s called on your system)!)