Specializing on keyword arguments

I have a function f(a; b=default_b, c=default_c), and want to implement a method f(a) = f(a, b=default_b, c=c_specialized_for_default_b), which should override f when neither b and c are provided. If I implement it like this, Julia does not choose the second method, but return Method definition overwritten (which is kinda expected, although we could hope Julia choose the latter one when calling f(a) by some dispatch-like reasoning).
I can implement it like this:

f(a; b=default_b, c=default_c)
    if b == default_b && c == default_c
        c = c_specialized_for_default_b
    end
    # some code
end

Is there a cleaner way (and maybe more efficient) to do this? Maybe there is a way to check if arguments are provided ?

I would use something like this, probably:

julia> function f(a; b=nothing, c=nothing)
           if isnothing(b) && isnothing(c)
               return _f(a)
           else
               return _f(a,b,c)
           end
       end
           
f (generic function with 1 method)

where _f(...) is the internal function for which the one with keyword arguments is the exposed interface.

3 Likes

As you noted keyword arguments don’t take part in dispatch, so you need to use some other mechanism. The primary options I know are

  1. Do it Python-style with f(a; b = nothing, c = nothing) and do isnothing checks to fill in desired defaults. This may have some type stability issues so it’s usually a good idea to have a function barrier before doing actual computations (i.e. call a new function or a different method of the same function).
  2. Define your function as f(a; kwargs...) and introspect kwargs to decide how to proceed with the call.

For the second option your example could be

function _f(a; b = default_b, c = default_c)
    # some code
end

f(a; kwargs...) = isempty(kwargs) ? _f(a; c = c_specialized_for_default_b) : _f(a; kwargs...)

For an example of complex keyword argument handling you can trace your way through this function: LongestPaths.jl/longest_path.jl at debd52b7bcaf59ceefac1d48935fbd8359dafb5b · GunnarFarneback/LongestPaths.jl · GitHub

5 Likes

Ok, thanks for your replies. So if I understand correctly, I need to change the definition of f(a::Type1; b::Type2=default_b, c::Type3=default_c) to f(a::Type1; b::Union{Nothing, Type2}=nothing, c::Union{Nothing, Type3}=nothing) (with the arguments management to call internal function that go with it) and there will be no breaking changes, am I correct ?

No need to be specific about the types, just use

f(a::Type1; b=nothing, c=nothing)

(unless there is a good reason to specify that further - performance is not affected by that).

1 Like

I think if your objective is just to change the default value of c when b has the default value then your original solution may be the best. Note that just because Julia does not dispatch on keyword arguments it does not mean it cannot specialize on them, if performance is what you are worried about.

My concern about performance was in the comparison on arguments to check if they are defaults, which could be costly if the objects are huge. Using nothing solves this, but this is not critical in my problem.

Ah, yes, in that case the solution with nothing has a clear advantage.