Unexpected (?) behaviour of `values()` for generic `kwargs...`

If I define a function with kwargs..., then I can use keys() to get the names of whatever arguments were passed

julia> function printkeys(; kwargs...)
           println(keys(kwargs))
       end
printkeys (generic function with 1 method)

julia> printkeys(a=1, b=2)
(:a, :b)

but if I try and use values(), I get a NamedTuple

julia> function printvalues(; kwargs...)
           println(values(kwargs))
       end
printvalues (generic function with 1 method)

julia> printvalues(a=1, b=2)
(a = 1, b = 2)

I’d actually have to call values() again on the NamedTuple to get what I originally expected

julia> function printvaluesvalues(; kwargs...)
           println(values(values(kwargs)))
       end
printvaluesvalues (generic function with 1 method)

julia> printvaluesvalues(a=1, b=2)
(1, 2)

I guess this just says that my expectations were wrong, but if I want a Tuple (or possibly Vector, or whatever) with just the values of the keyword arguments, is there a more idiomatic way to get it?

How about this?

julia> f(;kwargs...) = Tuple(v for (_, v) in kwargs)
f (generic function with 1 method)

julia> f(a=2, b=3)
(2, 3)

Another possibility that I just stumbled across is

julia> f(;kwargs...) = Tuple(kwargs.data)
f (generic function with 1 method)

julia> f(a=2,b=3)
(2, 3)

Which seems to be able to propagate values that are known at compile-time, in a way that I found useful for generic functions dealing with NamedDimsArrays. (Have to confess I haven’t tried @pfitzseb’s method yet).

If you access the data field in kwargs, then you are relying on the internal implementation details of Iterators.Pairs, which could change in any future 1.x version of Julia, so I wouldn’t rely on that approach.

2 Likes

Just a heads up to OP that this isn’t type-stable, if performance matters.

julia> f(;kwargs...) = Tuple(v for (_, v) in kwargs)
f (generic function with 1 method)

julia> @code_warntype f(a=2, b=3)
Variables
  #unused#::Core.Const(var"#f##kw"())
  @_2::NamedTuple{(:a, :b), Tuple{Int64, Int64}}
  @_3::Core.Const(f)
  kwargs...::Base.Iterators.Pairs{Symbol, Int64, Tuple{Symbol, Symbol}, NamedTuple{(:a, :b), Tuple{Int64, Int64}}}

Body::Tuple{Vararg{Int64, N} where N}
1 ─      (kwargs... = Base.pairs(@_2))
│   %2 = Main.:(var"#f#1")(kwargs...::Core.PartialStruct(Base.Iterators.Pairs{Symbol, Int64, Tuple{Symbol, Symbol}, NamedTuple{(:a, :b), Tuple{Int64, Int64}}}, Any[NamedTuple{(:a, :b), Tuple{Int64, Int64}}, Core.Const((:a, :b))]), @_3)::Tuple{Vararg{Int64, N} where N}
└──      return %2

Broadcasting getindex seems to be type-stable.

julia> f(; kw...) =  getindex.((kw,), keys(kw))
f (generic function with 1 method)

julia> @code_warntype f(a=2, b=3)
Variables
  #unused#::Core.Const(var"#f##kw"())
  @_2::NamedTuple{(:a, :b), Tuple{Int64, Int64}}
  @_3::Core.Const(f)
  kw...::Base.Iterators.Pairs{Symbol, Int64, Tuple{Symbol, Symbol}, NamedTuple{(:a, :b), Tuple{Int64, Int64}}}

Body::Tuple{Int64, Int64}
1 ─      (kw... = Base.pairs(@_2))
│   %2 = Main.:(var"#f#10")(kw...::Core.PartialStruct(Base.Iterators.Pairs{Symbol, Int64, Tuple{Symbol, Symbol}, NamedTuple{(:a, :b), Tuple{Int64, Int64}}}, Any[NamedTuple{(:a, :b), Tuple{Int64, Int64}}, Core.Const((:a, :b))]), @_3)::Tuple{Int64, Int64}
└──      return %2

julia> f(a=2, b=3)
(2, 3)
1 Like