Splitting kwargs into specific keyword arguments?

Let’s say I have these two functions:

function funA(; a=2.0)
    #something
end

function funB(; b=4.0)
    #something
end

They both take keywords arguments with default values. Now if I want to use these in a third function

function useAB(; a, b)
    return funA(;a=a) + funB(;b=b)
end

how could I write the third function useAB so that if I dont pass either a or b it defaults to using funA with its default value or funB with its default value? So I want to be able use useAB(), so that useAB() is equivalent to funA() + funB(), while being able to also have useAB(;a=3) to be equivalent to funA(;a=3) + funB() etc. I understand I could just have

function useAB(;a=2.0, b=4.0)
    #something
end

where the default values are the same, but the problem is that I don’t want to change the default values in two places if needed, or if e.g. funA is something sophisticated that does something specific when no arguments are passed(?).

Anyways, I started doing this by using

function useAB(;kwargs...)
    #something
end

and splitting the kwargs properly for the two functions, but the way I did it was very complicated. Also there is a danger of passing wrongly written arguments without the user noticing it. Is there a nice way of achieving this? Or should I somehow avoid this completely? If so, how?

Thank you for your time.

1 Like

As far as I am aware, this is not currently possible. You cannot dispatch on keyword arguments in general.

One workaround, off the top of my head, if you don’t mind using positional arguments for funA and funB:

funA(a=2.0) = a
funA(::Nothing) = funA()
funB(b=4.0) = b
funB(::Nothing) = funB()
useAB(; a=nothing, b=nothing) = funA(a) + funB(b)
1 Like

Something I have done is separate out the default values into separate functions, i.e.:

default_a() = 2.0
default_b() = 4.0
function funA(; a=default_a())
    #something
end
function funB(; b=default_b())
    #something
end
function useAB(;a=default_a(), b=default_b())
    return funA(; a) + funB(; b)
end

Then if I change the default values I only have to change it in one location.

Another advantage of this is that some keyword argument defaults depend on the values of other arguments, so the functions can handle that.

I’m curious how other people handle this situation.

6 Likes

Thanks for the ideas. The problem that I have with what @bgroenks suggested is that if funA and funB have several arguments, I would need to manually define every combination of them right? And then every combination again with useAB which would be a lot of work even for a couple of variables.

The problem that I have with what @mtfishman suggested is that I want to use the default values of a function I haven’t defined. So when I don’t pass any arguments I want the functions that I haven’t made to default to the behavior that they have when no arguments are passed.

I tried this approach:

function splitkwargs(kwargs, args...)
    for f in args
        isa(f, Function) ? nothing : throw(ArgumentError("args are not functions"))
    end
    allkwargs = []
    for f in args
        fkwargs = Base.kwarg_decl(first(methods((f)))) #gets the keyword arguments
        length(fkwargs) == 0 ? throw(ArgumentError(String(Symbol(f)) * " has no key-word arguments")) : push!(allkwargs, fkwargs)
    end
    allkwargs = [Base.kwarg_decl(first(methods((f)))) for f in args]
    wrongKeywordArguments = setdiff(keys(kwargs), union(allkwargs...))
    if length(wrongKeywordArguments) != 0 
        throw(UndefKeywordError(wrongKeywordArguments[1]))
    end
    out = []
    for kwarg in allkwargs
        #this puts the proper kwargs specific to a function in an array
        push!(out, (;[(key, kwargs[key]) for key in intersect(keys(kwargs), kwarg)]...))
    end
    if length(out) == 1
        return out[1]
    end
    return out
end

and you could use it like this

function useAB(; kwargs...)
    fAkwargs, fBkwargs = splitkwargs(kwargs, funA, funB)
    return funA(;fAkwargs...) + funB(;fBkwargs...)

I think it does what it is supposed to, BUT I tried using it with the apply function from the ITensors package and ran into the issue that apparently apply doesn’t have any keyword arguments. I read little bit about their code and they did something that when something like this

function apply(;kwargs...)
    keyarg1 = get(kwargs, :keyarg1)...
    #etc with the other args

So I guess they do not specify the keyword arguments so that the splitkwargs function doesn’t find them.

So I am not sure what to do about this. I think maybe I could use the splitkwargs function but pass the keywords that I want in arrays of symbols instead of trying to fetch them from the functions.

What do you think?

Here is an approach that you may be able to adapt to you needs.

We can define a “keyword-based constructor” for a struct type with default values as follows:

julia> Base.@kwdef struct Options
           a::Float64 = 2.1
           b::Int64 = 4
           c::String = "defaults"
       end
Options

julia> Options()
Options(2.1, 4, "defaults")

julia> Options(b = 16, c = "new settings")
Options(2.1, 16, "new settings")

Now define your specialised function like

julia> function funA(; opt = (; a))
           println("Value of option 'a' is: ", opt.a)
       end
funA (generic function with 1 method)

and so

julia> funA(; opt = Options())
Value of option 'a' is: 2.1

julia> funA(; opt = Options(a = 3.4))
Value of option 'a' is: 3.4

where the destructuring that occurs in funA only “sees” the a parameter, and accesses the default value automatically.

Similarly, you can define

julia> function funB(; opt = (; b))
           println("Value of option 'b' is: ", opt.b)
       end
funB (generic function with 1 method)

julia> funB(; opt = Options())
Value of option 'b' is: 4

which is selective for the relevant option b; now, this case be extended to the combined case

julia> function funAB(; opt = (; a, b))
           println("The sum of options 'a' and 'b' are: ", opt.a + opt.b)
       end
funAB (generic function with 1 method)

julia> funAB(; opt = Options())
The sum of options 'a' and 'b' are: 6.1

julia> funAB(; opt = Options(a=10, b=23))
The sum of options 'a' and 'b' are: 33.0

Another advantage of passing the Options struct is that you can define highly flexible Inner Constructor Methods to implement other logic.

EDIT: note also that if you want to be able to just call e.g. funA(), then you need an auxiliary definition like
funA() = funA(; opt = Options())

1 Like

Another option is this:

function funA(; a=2.0, kwargs…)
    #something
end

function funB(; b=4.0, kwargs…)
    #something
end

function useAB(; kwargs…)
    return funA(; kwargs…) + funB(; kwargs…)
end

Then whatever keywords are passed to useAB will be forwarded, and the others will use the default. If you use this approach, you have to use a set of unambiguous global keywords, as all functions can potentially see all keywords injected at the top function call.

1 Like

If I read the OP correctly, they don’t want default values to be “attached” to the function definition in order to avoid multiple locations of definitions.

1 Like

If you are responding to my post, it favors putting the default values at the lowest possible level, i.e. where the variable appears in the function body, and only there.

1 Like

Thanks for the suggestions. I like what @simsurace said, with that the default values will be defined only once and at the lowest level. But if there is a typo in the keyword when using useAB, will the kwargs... slurp in the misspelled keyword and not notify me about it? I don’t like being able to mistype a keyword and not be told about it, but maybe I’m too worried about it.

One problem I still have is that what would you do in the case where funB is a function from some package, so that it is defined without kwargs and I can’t access it to change it to be like that? So let’s say that in useAB I want to use the function funA defined by me, and the function notMyFunB which is not made by myself, so that I cannot change the argument definitions of notMyFunB, and notMyFunB takes in the kwarg b. How could I define the arguments in useAB so that when no kwargs are passed it uses the default values of funA and notMyFunB, but It would also be possible the pass the individual kwargs too? Sorry if this is getting confusing. I wish it would be as simple as

function useAB(; a=Nothing, b=Nothing)
    return funA(;a) + funB(;b)
end

without me having to explicitly define funA(::Nothing) = funA(), which gets tricky when funA has several kwargs.

I ended up using this

function splitkwargs(kwargs, args...)
    for f in args
        isa(f, Array{Symbol}) ? nothing : throw(ArgumentError("args are not arrays of symbols"))
    end
    wrongKeywordArguments = setdiff(keys(kwargs), union(args...))
    if length(wrongKeywordArguments) != 0 
        throw(UndefKeywordError(wrongKeywordArguments[1]))
    end
    out = []
    for kwarg in args
        push!(out, (;[(key, kwargs[key]) for key in intersect(keys(kwargs), kwarg)]...))
    end
    return length(out) == 1 ? out[1] : out
end

which could be used here like so:

function useAB(: kwargs...)
    #notmyfun has kwargs {calc, norm}
    funAkwargs, funBkwargs, notmyfunkwargs = splitkwargs(kwargs, [:a], [:b], [:calc, :norm])
    return funA(;funAkwargs...) + funB(;funBkwargs...) + notmyfun(;notmyfunkwargs...)

I’m not sure if there will be some problems with this approach, but oh well…

That‘s probably the main issue of the solution I proposed, and depending on the application, it could be quite serious.

I haven‘t tested this, but I think you should be able to check the keyword validity at the top entry point along the lines of issubset(keys(kwargs), VALID_KEYWORDS), where the second argument is a global constant list of Symbols, and throw an error otherwise. This prevents your mistyped options from being silently ignored. But to be sure, I would still recommend unit-testing all options for correct behavior even if typed correctly, as it is easy to forget to forward the kwargs.

1 Like