Finding the number of arguments of an anonymous function

I have an API wherein a kwarg is a function.

function apifunc(; f::Function = identity)
...
end

I would like to do something different depending on the number of arguments of f, so that apifunc(f = x -> 2) calls _apifunc1(f) but apifunc(f = (x,y) -> 2) calls _apifunc2(f). Is there a way to achieve that, preferably in a non-dynamical way? In other words, is there any way to do introspection on the allowed signatures of a function’s methods?

Functions don’t have this information, their methods do, eg

f() = 0
f(x) = 1
f(x, y) = 2

You would have to look up the method and from there it is possible to extract this information (using internals), but I would advise against it.

There is probably a cleaner and more idiomatic way to do what you want, but it is hard to say without more context.

julia> f = x::Int -> 2;

julia> methods(f)
# 1 method for generic function "#13":
[1] (::getfield(Main, Symbol("##13#14")))(x::Int64) in Main at REPL[19]:1

julia> m = first(methods(f))
(::getfield(Main, Symbol("##13#14")))(x::Int64) in Main at REPL[19]:1

julia> m.nargs
2

julia> m.sig
Tuple{getfield(Main, Symbol("##13#14")),Int64}

etc.

I wouldn’t base an API on this though.

2 Likes

To expand slightly on the previous answers: You might naively expect that the question is ill-defined for generic functions (that can have one method with one argument, and another method with two args, etc), but makes sense for anonymous functions.

This is not the case: Anonymous functions are just regular generic functions (with compiler-generated name instead of user-generated name) that can have many methods. You can add methods to an anonymous function by e.g.

julia> f_=x->x; (::typeof(f_))(x,y)=x+y; f_
#7 (generic function with 2 methods)

Even if there is only a single method, it still might be VarArg.

5 Likes

I wouldn’t base an API on this though.

I assume this is because this is not guaranteed to remain like this, yes?

I feel a bit hampered when dealing with Function objects because there is little information that I can access about them. They seem a bit black-box to me, as compared to standard types. methods seems like the main point of access to these, but it’s not the first time I’ve read that we should not use this in our packages.

There is probably a cleaner and more idiomatic way to do what you want, but it is hard to say without more context.

The context is this. I have a function hamiltonian!(h; onsitefield = (o, i) -> o) that takes a matrix h representing a Hamiltonian of a quantum system, and changes the diagonal element h[i,i] into onsitefield(h[i, i], i). Now, I also want to allow the user to say hamiltonian!(h; onsitefield = i -> some function of i), in which case h[i,i] should become onsitefield(i). I don’t know how to do this, since in my code I have to hardcode either onsitefield(h[i, i], i) or onsitefield(i). I would ideally like to use some form of dispatch to call one or the other, depending on the form of the onsitefield kwarg the user provides.

I come from Mathematica, where solving this is very easy. An anonymous function like f = x -> cos(x), which in Mathematica is written as f = Cos[#1]&, can be called with any number of arguments, and the excess ones are just dropped, i.e. I can write f[1,2,3,4,5] and this returns Cos[1] without complaining. This would solve the above problem, but I don’t know how to replicate that in Julia.

(By the way, I’m aware that f = (x, _...) -> cos(x) replicates the Mathematica functionality, but that would force the user to never use the onsitefield = i->... form.)

I am still not sure I understand the problem, but I guess the user could just pass a function that ignores the first argument if that is desired?

Yes, that is always an option: just demand that onsitefield have always the general form (o, i)-> .... I just hoped I could allow the user a simpler i -> ... syntax if her function does not depend on the existing onsite term o. If I do what you say, the user will just get a cryptic MethodError if she uses the simpler form.

(I do get the feeling that I’m probably trying to shoehorn a Mathematica way of thinking onto Julia…)

1 Like

julia> m = first(methods(f))

Ah, I see that there might be another reason for the reluctance to use methods:

julia> @btime methods($f)
  1.923 μs (12 allocations: 720 bytes)

I often use small structures when writing simulations where I want to try different behaviours of some sub-part. For example in a bigger simulation where I would use somewhere white noise or coloured noise, I would just define

struct WhiteNoise end
do_something_with(::WhiteNoise) = ...
struct ColouredNoise
    parameter::Float64
end
do_something_with(n::ColouredNoise) = ...

and write the rest of the simulation generic.

In your case you could wrap the functions into structures, e.g.

struct SingleArgumentFunction{F}
    f::F
end
struct TwoArgumentFunction{F}
    f::F
end

But there is certainly something more elegant along similar lines.

1 Like

Thanks @jbrea, but forcing the user to wrap the function in a wrapper is equivalent to forcing the user to always use a (o, i) -> ... syntax. What I would want is precisely a way to apply the correct wrapper (single or two-argument) to a given anonymous function automatically, depending on the number of arguments taken by its methods. But I now think this is not the correct way to go about this. What I want might actually not be possible/advisable at this stage yet.

PS: perhaps with metaprogramming… Something like @func i -> cos(i) transforming into (o,i) -> cos(i). That might be the better way.

You could do this by looking through the method table and checking for one or two argument methods and call whichever one exists but of course you’d have to make a choice when both arities of method exist for the argument. You could also error if the function has more than a single method but that seems a bit restrictive.

Yeah, that was @kristoffer.carlsson’s suggestion. He advised against it, however (did not specify the reason). I would actually not mind that at all if methods weren’t so slow…

You can try to call with two arguments, and if it fails, call the 1 argument form. I would guess it may be faster than inspecting the method table.

(In the REPL module, you can find in many places a callback which should accept 3 arguments IIRC, and typically these are written as (s, o...) -> ..., there indeed doesn’t seem to exist a direct solution.)

I’m afraid it is not

julia> t(f) = try; f(1); catch; f(1,2); end;
julia> h(x,y) = x+y;
julia> @btime t($h)
  25.134 μs (2 allocations: 48 bytes)
3

vs

julia> @btime first(methods($h)).nargs
  1.960 μs (12 allocations: 720 bytes)
3

Oh my, I did myself extremely basic benchmark and confused μs for ns :blush:

1 Like

This should be fast, but of course combines two evils:

Base.@pure function has_nargs(f, n)
    for m in methods(f)
        (m.nargs - 1) == n && return true
    end
    return false
end

function call(f)
    if has_nargs(f, 2)
        f(1, 2)
    else
        f(1)
    end
end
1 Like

HAHAHA!!! Nice! You used the Forbidden Word…

(I do not dare…)

@sdanisch, perhaps you could shine some light on the potential pitfalls of your approach? If I understand the implications well enough, I could be convinced to use this approach, but all the talk about missing backedges with Base.@pure and whatnot that apparently only Jameson understands scares me a little…

Why don’t you just document that the user should be giving a one argument function to the kwarg called onsitefield? If the function doesn’t have any single argument methods, you’ll get an error.