# What is the best way to require exactly 1 of 2 keyword arguments when writing a function?

I have a function “counts” that divides a sorted array of floats into subarrays based off a specified maximum value and an integer number of divisions, and then counts the length of each subarray, returning an array of lengths - the “counts” of each subsection. Currently it is something like

``````function counts(arr, max, divisions) ...
``````

called with
`counts(arr, 5.0, 5)`
This function works fine for this, but I would like to overload the function to be able to express the required subsections as either an integer number of divisions, or a float interval size. This could be easily achieved by

``````function counts(arr, max, divisions :: Int) ...
function counts(arr, max, interval :: Float64) ...
``````

However this could easily lead to misuse, where a user would get a very different result from using a float instead of an int. To avoid this, it would make sense to use keyword arguments, such that the function might be called with
`counts(arr, 5.0, divisions = 5)`
or
`counts(arr, 5.0, interval = 1.0)`
Thereby making it clearer to the user what they are doing. Exactly one of these should be required.
Here lies the problem. I can easily make keywords required by not assigning a default value

``````function counts(arr, max; interval :: Float64, divisions :: Int) ...
``````

but I don’t want both to be required, since either one is sufficient, and preferably I don’t want the user to try and specify both since it can only lead to mistakes. I don’t think I can do this in separate definitions as when they were not keywords, since the second just overwrites the first definition. One workaround I can imagine is to assign both keyword arguments a default value (such as 0) and then have the function check that exactly one is assigned to a non-zero value, and throw an error if not. That makes some sense since the given argument should not be 0 anyway, but manually checking that the function has been given the correct number of arguments feels wrong. Alternatively I could have an overload with one mode as a required keyword and the other not a keyword:

``````function counts(arr, max, divisions :: Int) ...
function counts(arr, max; interval :: Float64) ...
``````

which I have a feeling some native functions may do. Is this the best approach?

Here are a couple of options:

### Option 1: Multiple keywords and a run-time check:

Just set each keyword’s default to `nothing` and verify that one of them is not `nothing` inside the body of the function. This requires slightly more typing, but it should have little or no runtime cost.

``````julia> function counts(arr, max; interval::Union{Float64, Nothing} = nothing, divisions::Union{Float64, Nothing} = nothing)
if interval === nothing && divisions === nothing
error("Please provide `interval=` or `divisions=` as keyword arguments")
end
do_stuff()
end
counts (generic function with 1 method)
``````

### Option 2: Wrapper types

Create a wrapper type to indicate what kind of computation you are requesting, and then implement separate methods for those computations:

``````julia> struct Divisions
divisions::Int
end

julia> struct Interval
interval::Float64
end

julia> function counts(arr, max, divisions::Divisions)
do_stuff()
end
counts (generic function with 4 methods)

julia> function counts(arr, max, interval::Interval)
do_stuff()
end
counts (generic function with 4 methods)

julia> counts([], 1, Divisions(2))

julia> counts([], 1, Interval(2))
``````

This method will be easier to work with if you want to add some third method later on, since you just add more `struct` definitions and methods for `counts`, rather than having to edit an increasingly long list of keywords. This method should also impose little or no run-time cost.

9 Likes

I think @rdeits gave good solutions. Although instead of option 2 it might just be simpler to just use two different function names something like `countsByDivisions` and `countsByInterval`. Unless there is another reason to keep the function names the same.

1 Like

Option 3: Multiple keywords forwarded to multiple dispatch

``````count(arr, max; divisions::Union{Int,Nothing} = nothing, interval::Union{Float64,Nothing} = nothing) =
count_impl(arr, max, divisions, interval)
count_impl(arr, max, divisions::Int, interval::Nothing) = do_stuff()
count_impl(arr, max, division::Nothing, interval::Float64) = do_stuff()
``````

This is very similar to Option 1 but has zero runtime cost.

One drawback of Option 2 is that names like `Divisions` and `Interval` are likely to clash with other packages.

3 Likes

From the Book of Ill-advised Dispatch Patterns in Julia (Miskatonic University Press, 2027, forthcoming):

``````f(; kwargs...) = _f((; kwargs...))
_f(kwargs::NamedTuple{(:interval,)}) = "\$(kwargs.interval) interval"
_f(kwargs::NamedTuple{(:divisions,)}) = "\$(kwargs.divisions) divisions"
``````

In practice, I would go with Option 1 by @rdeits.

5 Likes

Thanks for the suggestions. I had thought of option 1 but I’m trying to avoid it on the principle that runtime checks for types should be a last resort. Option 2 is an interesting thought that I hadn’t considered. It certainly does what I want, although it feels slightly more convoluted than I like. I might avoid it on this occasion, but I will keep it as a tool for potential future use.

That… would make sense. In this circumstance it is as good a solution as any. It doesn’t exactly answer my question about keyword arguments but I will very likely end up doing this. I have a habit of trying to make use of overloading more than is perhaps wise…

FWIW, the code I posted doesn’t actually cause any cost at run-time because the compiler is smart enough to figure out what will happen just from the types of the arguments. Check out this example:

``````julia> function foo(x)
if x === nothing
return "hello world"
else
return 1.0
end
end
foo (generic function with 1 method)
``````

This looks like it’s type-unstable because it could either return a `String` or a `Float64` depending on the value of `x`. But the compiler is smart enough to know that the only case for which `x === nothing` holds is if `x` is of type `Nothing`. So it can actually figure out which branch of the `if` statement will be taken just from the type of the argument, and no checks have to happen at run-time. For example:

``````julia> @code_warntype foo(nothing)
Variables
#self#::Core.Compiler.Const(foo, false)
x::Core.Compiler.Const(nothing, false)

Body::String
1 ─ %1 = (x === Main.nothing)::Core.Compiler.Const(true, false)
│        %1
└──      return "hello world"
2 ─      Core.Compiler.Const(:(return 1.0), false)
``````
``````julia> @code_warntype foo(2)
Variables
#self#::Core.Compiler.Const(foo, false)
x::Int64

Body::Float64
1 ─ %1 = (x === Main.nothing)::Core.Compiler.Const(false, false)
└──      goto #3 if not %1
2 ─      Core.Compiler.Const(:(return "hello world"), false)
3 ┄      return 1.0
``````

The `if` statement has been removed at compile time, and there’s no actual type instability.

This is a pretty convenient pattern in Julia: just write out the code in a straightforward obvious way, including using run-time checks if that’s convenient. You can use `@code_warntype` to check for type instabilities and `@btime` (from BenchmarkTools) to check for performance problems and only go to more complicated type manipulation if it’s actually necessary.

3 Likes

That’s an interesting workaround. I think it’s probably the closest answer to what I wanted. Am I correct in thinking that `count(arr, max)` will still throw an error at runtime, but here explicitly using the internal type checking rather than a manual error branch?

Wow that’s impressive work from the compiler! I haven’t delved into those meta tools yet, I will have to give them a try.

Am I correct in thinking that `count(arr, max)` will still throw an error at runtime, but here explicitly using the internal type checking rather than a manual error branch?

Yes. Except that one could argue that the error occurs at compile time rather than runtime, but the difference isn’t all that relevant in Julia.

Here’s a concrete example:

``````julia> foo(;a::Union{Int,Nothing} = nothing, b::Union{Int,Nothing} = nothing) = foo_impl(a,b)
foo_impl(a::Int, b::Nothing) = println("foo(a = \$a)")
foo_impl(a::Nothing, b::Int) = println("foo(b = \$b)")

julia> foo()
ERROR: MethodError: no method matching foo_impl(::Nothing, ::Nothing)

julia> foo(a = 42)
foo(a = 42)

julia> foo(b = 42)
foo(b = 42)

julia> foo(a = 42, b = 42)
ERROR: MethodError: no method matching foo_impl(::Int64, ::Int64)
``````

As you can see, you indeed get an error for argument combinations that you did not define, but the error message might be a bit confusing for a user. But you can easily fix the error message by adding more methods to `foo_impl()`.