Forwarding vs filtering kwargs

When I want to forward kwargs from an API function to various internal dispatched methods I just allow them to have arbitrary kwargs, like so

external_api(x; kw...) = _internal(x; kw...)
_internal(x::Float64; kw1 = 1, kw...)  #only needs kw1, but allows any other
_internal(x::Int; kw2 = 2, kw...)  #only needs kw2, but allows any other

Is this the recommended pattern? The problem I see is that if I use a wrong keyword in the external_api, it will not complain. In other languages (e.g. Mathematica) there is a way to filter keywords allowed by a given function, so that if one uses a wrong keyword, it errors. Is there a similar pattern that one can use in Julia so that external_api(x; kw3 = 3) errors?

A bit off topic: What does Mathematica’s syntax for this look like?

You can always just write that yourself:

julia> allowed_kws = (:kw1, :kw2)
(:kw1, :kw2)

julia> function external_api(x; kws...)
           for kw in keys(kws)
               kw in allowed_kws || throw(KeyError(kw))
           end
           _internal(x, kws...)
       end
external_api (generic function with 1 method)

julia> _internal(x::Float64; kw1 = 1) = x+kw1
_internal (generic function with 2 methods)

julia> _internal(x::Int64; kw2 = 1) = x*kw1
_internal (generic function with 2 methods)

julia> external_api(2, kw3=3)
ERROR: KeyError: key :kw3 not found
1 Like

Thanks @pfitzseb, I guess that’s always an option. I just wanted confirmation that I wasn’t missing a more “correct” approach

You can also translate the keywords into a type, and then continue with that:

julia> Base.@kwdef struct Opts
       kw1 = 1
       kw2 = 1
       end
Opts

julia> function external_api(x; kws...)
       opts= Opts(;kws...)
       _internal(x, opts)
       end
external_api (generic function with 1 method)

julia> _internal(x, opts) = x +opts.kw1
_internal (generic function with 2 methods)

julia> external_api(4, kw1=7)
11

julia> external_api(4, kw3=7)
ERROR: MethodError: no method matching Opts(; kw3=7)
Closest candidates are:
  Opts(; kw1, kw2) at util.jl:728 got unsupported keyword argument "kw3"
  Opts(::Any, ::Any) at REPL[11]:2 got unsupported keyword argument "kw3"
Stacktrace:
 [1] kwerr(::NamedTuple{(:kw3,),Tuple{Int64}}, ::Type) at ./error.jl:125
 [2] (::getfield(Core, Symbol("#kw#Type")))(::NamedTuple{(:kw3,),Tuple{Int64}}, ::Type{Opts}) at ./none:0
 [3] #external_api#8(::Base.Iterators.Pairs{Symbol,Int64,Tuple{Symbol},NamedTuple{(:kw3,),Tuple{Int64}}}, ::Function, ::Int64) at ./REPL[12]:2
 [4] (::getfield(Main, Symbol("#kw##external_api")))(::NamedTuple{(:kw3,),Tuple{Int64}}, ::typeof(external_api), ::Int64) at ./none:0
 [5] top-level scope at none:0

If you use Paramters.jl’s @unpack at the top of each _internal method, then it is also pretty clear which options each method uses:

function _internal(x, opts)
  @unpack kw1 = opts
  ...
end

(I think if Opts was iterable, then kwargs and the ... syntax could still be used for the internal methods.)

3 Likes