KeywordCalls.jl

Author cscherrer
Popularity
6 Stars
Updated Last
1 Year Ago
Started In
April 2021

KeywordCalls

Stable Dev Build Status Coverage

In Julia, the named tuples (a=1, b=2) and (b=2, a=1) are distinct. In some cases, it's convenient to define a method for each set of names, rather than each particular ordering.

KeywordCalls.jl lets us do this, and allows specification of a "preferred ordering" for each set of arguments.

@kwcall

If we define

f(nt::NamedTuple{(:b, :a)}) = println("Calling f(b = ", nt.b,",a = ", nt.a, ")")

@kwcall f(b,a)

Then

julia> f(a=1,b=2)
Calling f(b = 2,a = 1)

julia> f(b=2,a=1)
Calling f(b = 2,a = 1)

We can define a new method for any set of arguments we like, including default values. If (after the above) we also define

f(nt::NamedTuple{(:c, :a, :b)}) = println("The sum is ", sum(values(nt)))

@kwcall f(c=0,a,b)

then

julia> f(a=1,b=2)
The sum is 3

julia> f(a=1,b=2,c=3)
The sum is 6

kwstruct

KeywordCalls is especially powerful when used for structs. If you have

Foo{N,T} [<: SomeAbstractTypeIfYouLike]
    someFieldName :: NamedTuple{N,T}
end

then

julia> @kwstruct Foo(μ,σ=1)
Foo

julia> Foo=2=4)
Foo{(:μ, :σ), Tuple{Int64, Int64}}((μ = 4, σ = 2))

In MeasureTheory.jl, we use this approach to allow multiple parameterizations of a given distribution.

Limitations

KeywordCalls tries to push as much of the work as possible to the compiler, to make repeated run-time calls fast. But there's no free lunch, you either pay now or pay later.

If you'd rather avoid the compilation time (at the cost of some runtime overhead), you might try KeywordDispatch.jl.

Benchmarks

Let's define a method for each "alphabet prefix":

letters = Symbol.('a':'z')

for n in 1:26
    fkeys = Tuple(letters[1:n])

    @eval begin
        f(nt::NamedTuple{$fkeys}) = sum(values(nt))
        $(KeywordCalls._kwcall(:(f($(fkeys...)))))
    end
end

So now f's methods look like this:

julia> methods(f)
# 28 methods for generic function "f":
[1] f(; kwargs...) in Main at /home/chad/git/KeywordCalls/src/KeywordCalls.jl:52
[2] f(nt::NamedTuple{(:a, :b, :c, :d, :e, :f, :g), T} where T<:Tuple) in Main at REPL[3]:5
[3] f(nt::NamedTuple{(:a, :b, :c, :d, :e, :f), T} where T<:Tuple) in Main at REPL[3]:5
[4] f(nt::NamedTuple{(:a, :b, :c, :d, :e), T} where T<:Tuple) in Main at REPL[3]:5
[5] f(nt::NamedTuple{(:a, :b, :c, :d), T} where T<:Tuple) in Main at REPL[3]:5
[6] f(nt::NamedTuple{(:a, :b, :c), T} where T<:Tuple) in Main at REPL[3]:5
[7] f(nt::NamedTuple{(:a, :b), T} where T<:Tuple) in Main at REPL[3]:5
[8] f(nt::NamedTuple{(:a,), T} where T<:Tuple) in Main at REPL[3]:5
[9] f(nt::NamedTuple{(:a, :b, :c, :d, :e, :f, :g, :h), T} where T<:Tuple) in Main at REPL[3]:5
[10] f(nt::NamedTuple{(:a, :b, :c, :d, :e, :f, :g, :h, :i), T} where T<:Tuple) in Main at REPL[3]:5
⋮
[26] f(nt::NamedTuple{(:a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m, :n, :o, :p, :q, :r, :s, :t, :u, :v, :w, :x, :y), T} where T<:Tuple) in Main at REPL[3]:5
[27] f(nt::NamedTuple{(:a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m, :n, :o, :p, :q, :r, :s, :t, :u, :v, :w, :x, :y, :z), T} where T<:Tuple) in Main at REPL[3]:5
[28] f(nt::NamedTuple) in Main at /home/chad/git/KeywordCalls/src/KeywordCalls.jl:50

That method 28 is the dispatch that requires permutation; it's called for any named tuple without an explicit method.

Now we can benchmark:

function runbenchmark()
    times = Matrix{Float64}(undef, 26,2)
    for n in 1:26
        fkeys = Tuple(letters[1:n])
        rkeys = reverse(fkeys)
        
        nt = NamedTuple{fkeys}(1:n)
        rnt = NamedTuple{rkeys}(n:-1:1)

        times[n,1] = @belapsed($f($nt))
        times[n,2] = @belapsed($f($rnt))
    end
    return times
end

times = runbenchmark()

Here's the result:

benchmarks

Required Packages

Used By Packages