Choose different behavior depending on possible arguments of user-defined function

A pattern I encounter every once in a while is that I want a user to be able to pass a function to one of my package’s functions. Then I’d like to do different things with this function depending on what arguments it can take. However, I’m not sure what the best way to do this is. I do think that given Julia’s multi-method functions, there should be a clean way of achieving this.

Here is an example where I use hasmethod, however that doesn’t really feel clean to me, as I assume hasmethod is a runtime check, and I would like something that feels more like dispatch.

function mapper(f, vec)
    if hasmethod(f, Tuple{eltype(vec), Int})
        map(enumerate(vec)) do (idx, v)
            f(idx, v)
        end
    else
        map(f, vec)
    end
end

userfunc1(x) = -x
userfunc2(i, x) = iseven(i) ? -x : zero(x)
julia> x = rand(5)
5-element Vector{Float64}:
 0.7023356049444345
 0.552363261191138
 0.6553850928756446
 0.9943377760767843
 0.45303097564896033

julia> mapper(userfunc1, x)
5-element Vector{Float64}:
 -0.7023356049444345
 -0.552363261191138
 -0.6553850928756446
 -0.9943377760767843
 -0.45303097564896033

julia> mapper(userfunc2, x)
5-element Vector{Float64}:
  0.0
 -0.552363261191138
  0.0
 -0.9943377760767843
  0.0

The idea is that the user can opt in to more complex behavior (in this case modifying the function application depending on the index of a value in the array) by passing in a Function that supports this.

Of course a function could have multiple methods so that it’s unclear which variant to choose, but in most cases that I encounter, these things would be anonymous functions where only one version is defined.

Behavior such as this could be achieved by passing boolean flags along with the function, however I would like it the most if the function itself could be what decides the behavior.

A pattern I encounter every once in a while is that I want a user to be able to pass a function to one of my package’s functions. Then I’d like to do different things with this function depending on what arguments it can take.

While I wish for something like this myself from time to time, I think it is not a strength of julia and I would avoid it. Personally I would force the user to be explicit about the signature of the passed function. Either by having variants like mapper_elements, mapper_pairs etc. Or by wrapping the function argument OnPairs(f), OnElements(f).
It is less elegant and less generic, but more robust and easier to debug.

1 Like

Probably more explicit with wrapper types, yes. But I still find it annoying that how a function behaves is information that is not really supposed to be accessed before applying it. I mean you can, with Base.return_types and applicable etc, but I rarely see that used in real code and it seems heavily discouraged.

I think the way Julia handles this is to let the user to choose the function. Thus, in this specific case you wouldn’t actually need the mapper function at all, if the user does:

julia> map(userfunc1, x)
5-element Vector{Float64}:
 -0.4434358244830304
 -0.23832820786718045
 -0.059860065319004896
 -0.7492146258258767
 -0.24561204189454466

julia> map(i -> userfunc2(i,x[i]), eachindex(x))
5-element Vector{Float64}:
  0.0
 -0.23832820786718045
  0.0
 -0.7492146258258767
  0.0

This of course requires the user to be more familiar with the syntax, but it is so general that it is hard to get a behavior as consistent as that.

2 Likes

I agree with Leandro on this one. This should not be something to the library writer to worry or consider. Either have a function that always expects a function that has a specific set of arguments, and it is the user responsibility to use anonymous functions to adequate what they have to pass to your function; or have multiple differently named functions, for the different kinds of functions you want the user to be able to pass (depending, adding a parameter to select between them is also a possibility).

2 Likes

This example is of course again just a bad illustration of what I really want to do (which is too niche a problem to be worth explaining here) but I guess the response was mostly expected. It’s probably just not a good idea to design an api around this kind of behavior in Julia.

1 Like

Well, the specific problem matters for the opinion on the specific solution. Maybe it makes sense for some niche case, but if you do not give us more information than that, well, then my previous opinion stands.

1 Like