This works as I would expect it to…
julia> f(x::Int) = "Int"
f(x::Float64) = "Float64"
@generated function g(x)
if hasmethod(f, Tuple{x})
return :(f(x))
else
return "???"
end
end;
julia> g(1)
"Int"
julia> g(1.0)
"Float64"
julia> g("a")
"???"
… but the following more clumsy way of writing an equivalent generated function throws an error:
julia> @generated function h(x)
if length(methods(f, Tuple{x})) > 0
return :(f(x))
else
return "???"
end
end;
julia> h(1)
ERROR: code reflection cannot be used from generated functions
Does that mean that the initial g(x) is also invalid but just misses an explicit sanity check?
Also, what exactly does “invalid” mean here? I’m aware of the problem that the generated functions do not get updated as more methods are added to f(x) and I’m okay with that. Is that the only surprise I should be expecting, or can either of these generated functions corrupt the entire Julia runtime? If the answer to the last question is yes, then what about the following workaround?
julia> hasmethod_f(_) = false
hasmethod_f(::Type{Int}) = true
hasmethod_f(::Type{Float64}) = true;
julia> @generated function i(x)
if hasmethod_f(x)
return :(f(x))
else
return "???"
end
end;
Edit: I’m basically repeating the question from Can I use hasmethod inside @generated function?. Maybe things have evolved in the meantime, and maybe someone could give a little more detail?
I don’t know the details, but calling methods seems to fulfill exactly point 5 in the documentation:
Generated functions must not mutate or observe any non-constant global state (including, for example, IO, locks, non-local dictionaries, or using hasmethod).
You would need to thread through the world age and pass it on to any method queries to make this query well-formed. If you are looking for an example of how to do that, take a look at handle world ages for julia v1.10 - Pull Request #40 - oxinabox/Tricks.jl - GitHub.
It’s worth asking yourself if you really need a generated function though. In recent Julia versions, hasmethod can be inferred statically by the compiler, so in almost all cases, you can just use a regular if statement without @generated. Even if you really need to use @generated you can call hasmethod outside the generated function and wrap it in Val, e.g.
cond = hasmethod(f, Tuple{T})
my_generated_function(Val(cond))
[...]
@generated function my_generated_function(::Val{cond}) where {cond}
if cond
...
else
...
end
end
For context, I’m currently working on a coordinate conversion package. The idea is, given a tuple of coordinates c (e.g. Cartesian coordinates c = (x,y,z)) and a target system s (e.g. s = Longitude()), I want to test whether there is a conversion method from any subset of c to s. So in the above example, I want to test whether I can convert from Cartesian x to Longitude() (I can’t), from Cartesian y to Longitude() (I also can’t), and so on, until eventually I’ll test (x,y) -> Longitude(), and that will work. If I find such a conversion method, then I want to use it, if I can’t then I want to try other manipulations on s and c and repeat the conversion test until eventually I find a conversion that works.
I’m aware that Julia is getting better and better at moving seemingly runtimey things to compile time without a human explicitly telling it to do so, but in my experience the combinatorial explosion of checks that I want to perform is at the brink of what Julia is capable of inferring statically. Static inference is absolutely crucial for my application, so inference working only sometimes was quickly becoming very frustrating. By contrast, @generated gives me explicit control over what happens at runtime and compile-time, and hence this feels like the more sane choice.
Thanks for the pointer! Unfortunately, I’m struggling quite a bit with that PR, though, so I’m not sure how to adapt it to my situation. Also, I want to ask once again, what exactly does “well formed” mean here? To my understanding, the main concern of the PR seems to be to add backedges from the generated function to all the methods that could invalidate it so that recompilation is triggered whenever new methods are added or existing ones are edited. I’m okay with giving up on this dynamic recompilation, so if that’s the only concern then I believe using hasmethod() should be fine for my use case. The only thing I want to 100% rule out is the potential runtime corruption alluded to in the documentation.
Mh, what might work is to use an @generated function to expand all the subset tests but then leave the hasmethod() check in the runtime part…