How can kwargs be used in multiple dispatch

Multiple dispatch usually works on variable types, I presumed it could also work using kwargs, but it doesn’t seem to. How can I make this work?

julia> function shout(word::String; exclaim::Int)
           println(word * "!"^exclaim)
       end
shout (generic function with 1 method)

julia> function shout(word::String; question::Real)
           println(word * "?"^question)
       end
shout (generic function with 1 method)

julia> shout("Hello", exclaim=3)
ERROR: UndefKeywordError: keyword argument question not assigned
Stacktrace:
 [1] top-level scope
   @ REPL[3]:1

julia> shout("Hello", question=3)
Hello???

I’m guessing the problem here is that an Int is a subset of Real, so calling shout(String, Int) is applicable to both methods.

2 Likes

Kwargs do not differentiate methods, so the Int version was overwritten (note the 1 method after the second definition).

I use this approach, in cases like this:

_f(a, b::Int) = 1
_f(a, c::Real) = 2
function f(a; b = nothing, c = nothing)
    !isnothing(b) && return _f(a,b)
    !isnothing(c) && return _f(a,c)
end

But I’m not a fan of it. Other opinions are welcome.

3 Likes

Thank you for your solution, I can see how it works but as you suggest it is possibly not the best. I won’t accept it as a solution yet in case someone has a more elegant one we can both benefit from.

1 Like

Another option is to use the package KeywordDispatch.jl.

3 Likes

If the example that you supplied is close to your actual usecase, the following appraoch would work:

julia> function shout(word::String, exclaim::Int)
           println(word * "!"^exclaim)
       end
shout (generic function with 1 method)

julia> function shout(word::String, question::Real)
           println(word * "?"^question)
       end
shout (generic function with 2 methods)

julia> function shout(word::String; kwargs...)
           return shout(word, last(only(kwargs)))
       end
shout (generic function with 3 methods)

Note, that in the first two methods, the second argument is not a keyword argument, but positional and thus dispatch works as expected.
The last method can take any keyword argument and simply passes its value on to either of the two other methods.

EDIT: If, however, you have more than one keyword argument you would need an intermediate step, that passes the arguments on in the correct order.

EDIT2: Unfortunately, this still does not work, as the question-method needs an Integer to work, but then it is dispatched to the exlaim-method. It is generally bad practice to suggest via the method signature, that the method would work with any Real, when it actually requires an Integer.
But if the two types Real and Integer where not related, this way would work.

EDIT3: The easiest way to solve your problem (although maybe not how you intended) would simply be:

julia> function shout(word::String; exclaim=0, question=0)
           println(word * "?"^question * "!"^exclaim)
       end
shout (generic function with 3 methods)
3 Likes

Again, thank you for your solution, I think it is still rather complex as highlighted with edits one and two.

You have successfully dissected my MRE to a point where it doesn’t replicate my actual problem which is fun to see :slight_smile: Unfortunately, my actual use case requires Int and Real.

Edit 3 is interesting, it would be a more pythonic/ditch multiple dispatch approach which would work too. I’d have to have an if statement though, so it’d be more like

function shout(word::String; exclaim::Union{Nothing, Int}=nothing, question::Union{Nothing, Int} =nothing )
    if exclaim === nothing && question === nothing
        error("Only one of exclaim or question can be specified")
    elseif !isnothing(exclaim)
        println(word * "!"^exclaim)
    elseif !isnothing(question)
        println(word * "?"^question)
    else
        error("One of exclaim or question must be specified")
    end
end

Although this would be its most verbose form, if after refactoring it would lose the beauty of multiple-dispatch.

Thank you for linking this package, I wasn’t aware of it. It should be perfect but I cannot get it to work as desired, I get the same error as in the initial example

julia> @kwdispatch shout()
julia> function shout(word::String; exclaim::Int)
                  println(word * "!"^exclaim)
              end
shout (generic function with 2 methods)
julia> function shout(word::String; question::Real)
                  println(word * "?"^question)
              end
shout (generic function with 2 methods)
julia> shout("Hello", exclaim=3)
ERROR: UndefKeywordError: keyword argument question not assigned
Stacktrace:
 [1] top-level scope
   @ REPL[34]:1
julia> shout("Hello", question=3)
Hello???

I have also tried @kwdispatch shout(word::String) in case the common argument is required. In the examples given they only seem to have kwargs, so I’m not sure if there is a problem having positional and keyword?

The way to sort of use multiple dispatch here is to do this:

julia> _shout(word::String, exclaim::Int, question::Nothing) = println(word * "!"^exclaim)
_shout (generic function with 1 method)

julia> _shout(word::String, exclaim::Nothing, question::Int) = println(word * "?"^question)
_shout (generic function with 2 methods)

julia> _shout(word::String, exclaim::Int, question::Int) = error("Only one of exclaim or question can be specified")
_shout (generic function with 3 methods)

julia> shout(word::String; exclaim=nothing, question=nothing) = _shout(word, exclaim, question)
shout (generic function with 1 method)

julia> shout("hello"; exclaim=1)
hello!

julia> shout("hello"; question=1)
hello?

But I am not sure if that is cleaner than the simple branches. I opted for the branches last time I had to do that.

2 Likes

That is strange. The following code works for me:

 using KeywordDispatch

 @kwdispatch shout(word::String)

 @kwmethod function shout(word::String; exclaim::Int)
    println(word * "!"^exclaim)
end

@kwmethod function shout(word::String; question::Real)
    println(word * "?"^question)
end

shout("Hello", exclaim=3)

shout("Hello", question=3)

I’m using Julia 1.8.0.

1 Like

I guess, in principle, EDIT2 would be the right way for your requirement. The only problem is, that exclaim method takes an Int and the question-method a Real (i.e. the supertype of both Integer and AbstractFloat). This means, that via normal dispatch you’re only able to reach the question-method if the argument is not <:Integer (which probably is the reason, that you are reaching for keyword arguments in the first place).
That said, one way to get around this is with value types, that allow you to dispatch on values, rather than types:

julia> function shout(word::String; kwargs...)
           key,value = only(kwargs)
           shout(word, value, Val{key}())
end
shout (generic function with 1 method)

julia> function shout(word::String, question::Real, ::Val{:question})
                  println(word * "?"^question)
end
shout (generic function with 2 methods)

julia> function shout(word::String, exclaim::Int, ::Val{:exclaim})
                  println(word * "!"^exclaim)
end
shout (generic function with 3 methods)
1 Like

I have this working now, when including the positional arg in @kwdispatch I was forgetting to use @kwmethod. It is unfortunate that KeywordDispatch cannot have default arguments though as my real use case would benefit from this. But that wasn’t the original question, thank you for your solution

2 Likes

Thank you for all your solutions, I need to think about what suits my application best as some of these options are less intuitive than regular multiple dispatch. For code clarity it might be best to have two distinct functions.