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.
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. If (after the above) we also define
f(nt::NamedTuple{(:c, :a, :b)}) = println("The sum is ", sum(values(nt)))
@kwcall f(c,a,b)
then
julia> f(a=1,b=2,c=3)
The sum is 6
KeywordCalls is especially powerful when used for structs. If you have
Foo{N,T} [<: SomeAbstractTypeIfYouLike]
someFieldName :: NamedTuple{N,T}
end
then
julia> @kwstruct Foo(μ,σ)
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.
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.
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: