Say there’s a type T, how can I tell if function (t::T)()… is defined?
Is there any way other than trying t() and catching a MethodError?
maybe using methods
?
Trying and catching is, unfortunately, the easiest/surefire way. Checking methods
is tricky, and if you need to check, using applicable
with a specific signature is better (though not by much).
Maybe you could define an abstract type and have the callable types subtype it?
Could you elaborate on the issues that might arise using methods
or hasmethod
/applicable
?
Because you need an instance of the object to determine it - the type is not enough. The methods are attached to “objects of type T
”, not to the type object T
.
The issues I’m referring to are potential confusion about what methods
/applicable
tells you:
julia> struct Foo{T} end
julia> (f::Foo{T} where T <: Integer)() = 1
julia> methods(Foo{Int})
# 1 method for type constructor:
[1] (var"#ctor-self#"::Type{Foo{T}} where T)()
@ REPL[3]:1
julia> applicable(Foo{Int})
true
All good, right? Not so fast, calling that is the constructor method for Foo
(as the printout for methods
suggests):
julia> Foo{Int}()
Foo{Int64}()
and only if we pass in an instance (which we could then just call anyway) do we get out the 1
:
julia> methods(Foo{Int}())
# 1 method for callable object:
[1] (f::Foo{T} where T<:Integer)()
@ Main REPL[4]:1
julia> applicable(Foo{Int}())
true
julia> Foo{Int}()()
1
The reason I’m recommending applicable
over inspecting methods
is that there are situations where throwing isn’t really a good option either, where it’s safer to have actual arguments you’ll try to pass that you want to check.
Needing an instance t
to check for methods (t::T)(args...
) is probably rooted in methods
/hasmethod
/applicable
and a whole lot of other functions being intended for generic functions, where it is easy to find the instance. Might be a pain to type typeof(foo)
and Type{T}
instead, but it’d be nice if those functions took the type instead of the callable instance.
Note that Function
is an abstract type, so you are free to declare struct Foo <: Function
. (Not all callable types do this, however.)
That wouldn’t solve the issue though; the type of an instance would give you its type, methods
on which just gives you the constructor, as above. What then is the type of typeof(Int)
?
What’s leaking here is that julia doesn’t really have higher kinded/dependent types - julia answers the above question by saying typeof(DataType) == DataType
.
Note that this is just a user facing API problem, not a fundamental problem.
julia> struct Foo{T} end
julia> (f::Foo{T} where T <: Integer)() = 1;
julia> Core.Compiler._methods_by_ftype(Tuple{Foo{Int}}, -1, typemax(UInt))
1-element Vector{Any}:
Core.MethodMatch(Tuple{Foo{Int64}}, svec(), (f::Foo{T} where T<:Integer)() @ Main REPL[22]:1, true)
methods
and applicable
take instances of the type, but the compiler itself operates on the type level. We tell the difference between (::T)(x)
and T(x)
by distinguishing between Tuple{T, typeof(x)}
and Tuple{Type{T}, typeof(x)}
:
julia> Core.Compiler._methods_by_ftype(Tuple{Type{Foo{Int}}}, -1, typemax(UInt))
1-element Vector{Any}:
Core.MethodMatch(Tuple{Type{Foo{Int64}}}, svec(), (var"#ctor-self#"::Type{Foo{T}} where T)() @ Main REPL[21]:1, true)
I’m using the user-facing API, because that is much more reliable when it comes to talking about what people can actually use. I can’t reproduce this on a more recent commit of julia:
julia> struct Foo{T} end
julia> (f::Foo{T} where T <: Integer)() = 1;
julia> Core.Compiler._methods_by_ftype(Tuple{Foo{Int}}, -1, typemax(UInt))
julia> Core.Compiler._methods_by_ftype(Tuple{Type{Foo{Int}}}, -1, typemax(UInt))
julia> versioninfo()
Julia Version 1.10.0-DEV.1525
Commit 0da46e25c86 (2023-06-20 02:23 UTC)
Platform Info:
OS: Linux (x86_64-pc-linux-gnu)
CPU: 24 × AMD Ryzen 9 7900X 12-Core Processor
WORD_SIZE: 64
LIBM: libopenlibm
LLVM: libLLVM-15.0.7 (ORCJIT, znver3)
Threads: 34 on 24 virtual cores
Environment:
JULIA_PKG_USE_CLI_GIT = true
You may say that it’s just a user-facing API problem, but that really just hides the underlying issue of typeof(typeof(Int)) == typeof(Int)
being DataType == DataType
.
EDIT: took a while to find, but the reference about “what is the type of DataType
?” I had in mind was type universe in nLab
If the reason you are avoiding catching MethodError is for speed, you could see this thread:: Performance of hasmethod vs try-catch on MethodError.
The solution is overkill (works by caching the MethodError
s) but it works, is type stable, and only faces a ~10 ns overhead.
I’m not at all suggesting people reach into the internals for this, I’m just giving some more context for what specifically is missing here, which is not some fundamental problem that requires changing the type system but some missing methods / functions.
Again though, that underlying issue isn’t fundamental but about APIs. The entire reason Type
exists, because it’s exactly meant to be the type of types:
julia> Int isa Type{Int}
true
I definitely agree that it’s annoying that typeof(Int)
gives DataType
though, because we’re all used to using typeof
for data. But it is a further API issue that we don’t expose Core.Typeof
instead of typeof
julia> Core.Typeof(1)
Int64
julia> Core.Typeof(Int)
Type{Int64}
At the end of the day though, yes I agree if all you have is T
it’s very hard to find out if there are methods on instances of T
, I’m just pointing out that these aren’t core problems with the type hierarchy or whatever, but problems which how APIs are set up.
No, I’m specifically talking about typeof
, because that’s what the user-facing, stable API is, and what the julia type lattice consists of. Core.Typeof
can do whatever it wants to; it’s internal. From the user perspective, changing typeof
to match Core.Typeof
(while allowing such nested type shenanigans), is a big change, and exactly something that would be required to soundly talk about doing such queries in user code.
If you need internals to even access this kind of type computation, it’s not at all something that the (user facing) type system (i.e., the lattice spanned by <:
on typeof
of objects) can “do” (even if the underlying computation can be done). If you change typeof
to Core.Typeof
you can do pretty much whatever you want, because it’s a different lattice with different objects on that lattice, due to Core.Typeof
giving a much bigger set of possible types/objects.
Note that there is already Base.Callable
defined in essentials.jl to be the union of Function
and Type
. It is used to guide multiple dispatch for methods like get!
in dict.jl.
Is that really a problem for figuring out if a given type’s instances are callable? Here’s the three cases I can think of:
- generic function: providing
typeof(foo)
is as specific asfoo
itself, being a singleton type - constructor: we can provide
Type{T}
manually, and a method can extract theT
from an argument::Type{Type{T}}
and then do what the existing methods do. - all other callable instances: providing
T
is sufficient for finding a method(t::T)(args...)
. If we provide an instancet
, the instance values are just ignored.
We don’t need to implement (1) and (2) because the callables of interest are accessible, but a new method that does (3) and also happens to handle (1) and (2) seems feasible. An issue for (2) though, @nospecialize
can’t do unnamed arguments, and it doesn’t seem like @nospecialize(r::Type{R})
prevents specialization, based on (@which ...).specializations
.