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)!)