How do I tell if an object is callable (a function-like object)?

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?

1 Like

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.

2 Likes

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)
1 Like

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 MethodErrors) 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:

  1. generic function: providing typeof(foo) is as specific as foo itself, being a singleton type
  2. constructor: we can provide Type{T} manually, and a method can extract the T from an argument ::Type{Type{T}} and then do what the existing methods do.
  3. all other callable instances: providing T is sufficient for finding a method (t::T)(args...). If we provide an instance t, 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.

1 Like