# How to determine the methods for a callable object without instantiating it?

Suppose I have the following:

``````struct A{T} x::T end
(a::A)(y) = a.x+y
``````

Then I can say

``````julia> a = A(1); methods(a)
# 1 method for callable object:
[1] (a::A)(y)
@ Main REPL[2]:1
``````

This list only depends on the type of `a`, not on the element `a` itself. Is it possible to get this list directly from the type `A{T}`, without instantiating any element?

2 Likes

Yes, it is possible. But from what I can see this is not supported yet.

However, you can use the following function to achieve the desired functionality:

``````function fancymethods(x)
tsyg = Tuple{x,Vararg{Any}}
temp = Base._methods_by_ftype(tsyg, -1, Base.get_world_counter())
ms = Method[]
for m in temp::Vector
m = m::Core.MethodMatch
push!(ms, m.method)
end
Base.MethodList(ms, x.name.mt)
end
``````

It required a little digging, and I don’t have much time to explain everything right now (if a detailed explanation is needed, I can provide it later).

This is not defined under `Base` and I didn’t test it with types defined under various modules - so I cannot provide many guarantees at this point. I’ll make it more robust later.

The usage differs from parametric struct vs non-parametric.

Examples:

``````struct A{T}
x::T
end
(a::A)(y) = a.x + y
(b::A)(x::String) = string(b.x) * x
(c::A{Float64})(x::Float64) = c.x + x

fancymethods(A{Any})
``````

The above `fancymethods` call will produce:

``````# 2 methods for callable object:
[1] (b::A)(x::String)
@ Main ~/code/teach.jl/cobj.jl:5
[2] (a::A)(y)
@ Main ~/code/teach.jl/cobj.jl:4
``````

You can see that the `A{Float64}` was ignored.

However, calling `fancymethods(A{Float64})` will produce, as expected:

``````# 3 methods for callable object:
[1] (b::A)(x::String)
@ Main ~/code/teach.jl/cobj.jl:5
[2] (c::A{Float64})(x::Float64)
@ Main ~/code/teach.jl/cobj.jl:6
[3] (a::A)(y)
@ Main ~/code/teach.jl/cobj.jl:4
``````

The idea is that you don’t need to use an instance element; the type will suffice. But you need to be careful since `A{TypeA}` is not necessarily the same as `A{TypeB}` (e.g., the returned methods follow the dispatch rules).

For non-parametric types, things are straightforward:

``````struct B
x::Int
end

(b::B)(x::String) = string(b.x) * x
(b::B)(y) = b.x + y

fancymethods(B)
``````

Will produce:

``````# 2 methods for callable object:
[1] (b::B)(x::String)
@ Main ~/code/teach.jl/cobj.jl:20
[2] (b::B)(y)
@ Main ~/code/teach.jl/cobj.jl:22
``````

The `fancymethods` function I wrote above is specially designated to get the callable object methods from using the type instead of the instance (if no method is defined for the object, you’ll get an empty `MethodList`).

Rely on the standard `methods` for everything else.

Have fun.

P. S. I think this would be helpful to be part of `Base` (the API should be something like `methods(T, withobjectmethods=true)` - this would return both the constructors for T and the callable object methods - or maybe the keyword can be used to indicate that only the callable object-related methods should be returned).

3 Likes

That’s expected though. `A{Float64}` is not a subtype of `A{Any}`, both are concrete types that subtype `A`.

Playing with this a little, `fancymethods(A)` doesn’t work (throws error for `UnionAll`), and declared-`abstract` types will include the concrete subtype methods (I subtyped `B <: C` in your example to check), which I’d sometimes want to exclude. Still, it’s incredibly useful as is, I wish more reflection methods accepted a callable’s type.

I’d rather a keyword that specifies where I’m checking a callable or a callable’s type, so `methods(T, ..., :callable)` for `T(...)` methods and `methods(T, ..., :typeofcallable)` for `(::T)(...)` methods.

2 Likes

This extends the original functionality to include `UnionAll` scenario as well:

``````function fancymethods(x)
tsyg = Tuple{x,Vararg{Any}}
temp = Base._methods_by_ftype(tsyg, -1, Base.get_world_counter())
ms = Method[]
for m in temp::Vector
m = m::Core.MethodMatch
push!(ms, m.method)
end
mt = x isa UnionAll ? Base.unwrap_unionall(x).name.mt : x.name.mt
Base.MethodList(ms, mt)
end
``````

The `Base.get_methodtable` fallback seems sound and the few tests I am running are looking good. I’ll keep this approach until disproved by some weird outcome.

Now the `fancymethods(A{<:Any})` call works OK.

3 Likes

That’s great, thanks a lot! I also think that this functionality would be valuable in Base. If one adds it, then maybe one should allow empty lists to be returned, as is the case with `methods`:

``````julia> struct A end; methods(A())
# 0 methods for callable object
``````

Updated the solution to reflect this.

This can be easily adapted to work via `methods` the way @Benny suggested.

@gdalle, if you find this valuable and already have the Julia repo setup, you might find this as a low-hanging fruit - also, I think this is worthy of `Base` addition. Pointing you to the method that needs extending here.

@algunion I do like low-hanging fruit but these days I’m on a fruit-heavy diet already Could this be your opportunity to overcome the impostor syndrome and venture bravely into PR-land?

5 Likes

I’ll do it this week sometime: it looks like more work to set up the whole repo deal than doing the PR But I understand that is a one-time thing.

4 Likes

Done.

6 Likes

The PR using that keyword `methods(T, :callable)` to find methods `(::T)(args...)` is a bit cryptic because every input is a callable, including functions, type constructors, and callable instances we can use `methods` for already. Earlier I tried to distinguish the current usage and the proposed type-level usage with `:callable` and `:typeofcallable`, but in retrospect `:callable` by itself gives little information. Perhaps just directly with `:instance` vs `:type`? `:instance` (or omitting the argument entirely) would do what `methods` does already, while `:type` does the new stuff, in other words `methods(f, [types], [module]) == methods(f, [types], [module], :instance) == methods(typeof(f), [types], [module], :type)`. Still not a final idea because that may be misconstrued as referring to the arguments, but `:callableinstance` and `:typeofcallable` seems a bit verbose.

It just occurred to me, `which` has a method that takes a type signature as a tuple type of the types of the callable and the arguments. That seems clearer and easier than a trailing keyword.

1 Like

It didn’t seem feasible to keep the surface as simple as possible and cover all the cases without adding much code. And the simplest option seemed only to introduce a singular keyword - `:callable` (with the meaning of `callable object of type T`).

So, as a general rule, the following assumptions need to hold:

``````struct A{T}
x::T
end

(a::A{Float64})(x::Float64) = a.x + x
(a::A{String})(x::String) = a.x * x

# make `a` instance
a = A(...)

methods(A) != methods(A, :callable)
methods(a) == methods(typeof(a), :callable)
methods(a) == methods(a, :callable)

methods(push!) == methods(push!, :callable)
``````

So, using `:callable` on an instance will just be ignored.

I tend to agree with the semantics part of things: especially after reading your message, maybe `:instance/:type` would be a better fit. And I would also avoid the verbose variants at this point.

At the time of writing the code + docs, my brain pretty much used the `:callable` idea in the context of `callable object` or `functor` (and I would avoid using `functor`).

Doesn’t `methods(T, :instance)` seems to imply returning the methods for the instance of `T`? Which is not the default behavior.

It feels like those discussions about discussions, in a way - we are in the meta realm now.

Wish it had occurred to me sooner but I edited that comment to point out `which` has a (undocumented) method that takes a tuple type of the types of the callable and the arguments together e.g. `which(Tuple{typeof(+), Int, Int})`. Seems clearer because you know it’s a callable by it preceding arguments, and you know it’s the type of the callable because it comes with types of the arguments, don’t need to look out for a trailing keyword. An instance of that tuple type would just be call inputs e.g. `(+, 1, 2)`. This signature-type pattern shows up in other methods that just don’t make it to the documentation, so it seems like a precedence to adhere to. Just need to figure out how to write a signature without specified arguments…

A. S. For potential readers not aware of the full context: please be advised that the functionality discussed on this topic is not part of the Julia language at this point. You can use the function selected as `solution` if you need to achieve the behavior stated in the OP.

If we put aside for a moment the actual keyword name, what you say above is already compatible with both default `methods` implementation and the `:callable` flavor.

``````struct A{T}
x::T
end

(a::A{Float64})(x::Float64) = a.x + x
(a::A{String})(x::String) = a.x * x
(a::A{String})(x::Symbol) = a.x * string(x)

# make A{String} instance
a = A("ha")

methods(a, Tuple{Symbol}) == methods(A{String}, Tuple{Symbol}, :callable)
``````

Basically, the `:callable` just enables the caller to use the type instead of the instance in the `methods` calls and get out the same answer as you would get when using the instance.

The `methods` behavior is untouched in all other aspects.

Also, when printing the methods list, you always get the appropriate message: `methods for callable object` vs `methods for type constructor` (and the `methods for generic function`, etc. are left alone anyway).

One solution to drop the keyword usage altogether might be the creation (inside `Base`) of yet another convenience/wrapper type:

``````Instance{T}
t::T
end
``````

So `methods(T)` would return the type constructor methods (the current behavior), and `methods(Instance{T})` would return the methods defined for instances of `T` (e.g., methods for the callable objects).

While technically possible, the `Tuple{}` usage (specifically the way it is used in `which`) seems like departing from the regular use of `methods` (where you merely return methods for the first argument and optionally restrict/filter by a second `Tuple{}` argument).

True, but we’re already departing from the regular use by invoking the callable’s type and possibly a new argument. Also checking the source code, `methods` with unspecified argument types does `methods(f, Tuple{Vararg{Any}}, mod)`, and ultimately does use a helper `_methods_by_ftype` that takes in a signature tuple type like `Tuple{typeof(f), Vararg{Any}}`.

If we let the user-level `methods` take in this type, we could also make a helper method to make it. I’m imagining that people don’t want to manually type out an entire signature type, which they often don’t even do for the arguments in `methods`, they may at most specify number of arguments. So this may help:

``````typesig(T,N) = Tuple{T, Vararg{Any, N}} # becomes Tuple{T, Any, Any, ...}
typesig(T) = Tuple{T, Vararg{Any}}
# potentially, methods(typesig(T))
``````

Aesthetically, it doesn’t really save typing compared to a trailing key word, and it’s about the same clarity as `Instance{T}` with the key word in the lead. Downside of the latter is that `Instance{T}` itself is a callable struct, so it’s ambiguous whether `methods` treats it as callable instance (like it can now) or type (in which case `Instance{T}` itself becomes hard to inspect methods for). Admittedly, the tuple signature does the same thing because tuple types are callable (for example, you can do `methods(Tuple)` or `methods(Tuple{typeof(+), Int, Int})` now), but there’s more precedence for it. Either way could be breaking though, which needs to be put in a separate function.

A separate function certainly won’t have to worry about collisions with the current usage. It also occurs to me that this could just be released as a v0 package, which people can experiment with, without the promise of backward compatibility of merging into v1 Julia. People can also contribute `typeof(callable)` versions of other standard library reflection methods, and it may eventually be absorbed into base Julia.

1 Like

Another idea for non-breaking trailing key words: `:f` vs `:ftype`, reflecting some of the naming conventions in reflection.jl. The documentation already shows `methods(f, [types], [module])` to use `f` to indicate a callable instance, so a new method’s documentation could say `methods(ftype, [types], [module], :ftype)`, concise and seems clear enough. I’m still partial to a clean signature type-based reflections package, but keeping the callable and the arguments separate in 1 existing function seems more realistic.

Also, we may have incorrectly assumed a new argument must be put at the end. I think it may be possible for new methods to dispatch on the 2nd argument as `::Symbol`, so it could be `methods(ftype, :ftype, [types], [module])`.

Could also make it a keyword-only argument `methods(Foo; mode=:ftype)` just in case adding another optional positional argument is too radical. It’s not like this argument needs to participate in multiple dispatch.

1 Like

@Benny , thank you for the insights.

I’ll not be able to put much work into this in the following days - but before updating that PR, I would really like to somehow validate this (and your idea of a standalone package where we can iterate fast seems attractive indeed).

I wrote this a while back - and this was the exact rationale: to have a fast-moving environment where we can iterate/validate ideas. Also, don’t wait for the slow-paced review process (and this is not a critique - I just admit the existing/expected constraints) before making valuable stuff available to the community.

I might revisit that idea.

1 Like

After discussions here, I opted for `instancemethods` flavor and arrived at the following PR: