Splatting tuples into a named tuple implicitly converts them to pairs?

I was reading the docstring for NamedTuple and I discovered something odd. You can create a named tuple using pairs, like this:

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

Trying to do the same thing with tuples instead of pairs doesn’t work:

julia> (; (:a, 1), (:b, 2))
ERROR: syntax: invalid named tuple element "(:a, 1)"
Stacktrace:
 [1] top-level scope at REPL[3]:1

That’s not the surprising part. The surprising part is that splatting an array of tuples into a named tuple magically works:

julia> args = [(:a, 1), (:b, 2)];

julia> (; args...)
(a = 1, b = 2)

Given that (; (:a, 1), (:b, 2)) does not work, I would think that splatting tuples into a named tuple would not work either. Is the splatting implicitly converting the tuples to pairs? I wouldn’t think that would be the case either, since splatting tuples into a dictionary doesn’t work (i.e., the tuples are not converted to pairs):

julia> Dict(args...)
ERROR: MethodError: no method matching Dict(::Tuple{Symbol,Int64}, ::Tuple{Symbol,Int64})
Closest candidates are:
  Dict(::Any) at dict.jl:127
Stacktrace:
 [1] top-level scope at REPL[8]:1

Can anyone explain this apparent inconsistency?

3 Likes

you can splat a dictionary that has symbols as its keys

julia> dict = Dict([:integer => 1, :animal => "aardvark"])
Dict{Symbol,Int64} with 2 entries:
  :integer => 1
  :animal => "aarkvark"

julia> (; dict...)
(integer = 1, animal = "aarkvark")

Ok, I think I figured it out. What I didn’t realize was that an iterator of 2-tuples works just as well as an iterator of pairs when splatting into the kwargs... slot of a function. This is true for any function of the form f(<args>; kwargs...), not just named tuples.

First, some set up:

foo(; kwargs...) = kwargs;
bar(args...) = args;

args_pairs = [:a => 1, :b => 2];
args_tuples = [(:a, 1), (:b, 2)];

Now let’s compare args_pairs and args_tuples:

julia> foo(; args_pairs...)
pairs(::NamedTuple) with 2 entries:
  :a => 1
  :b => 2

julia> foo(; args_tuples...)
pairs(::NamedTuple) with 2 entries:
  :a => 1
  :b => 2

When they are splatted into keyword arguments, the iterator of pairs and the iterator of 2-tuples are exactly equivalent. However, when they are splatted into positional arguments, they are not equivalent:

julia> bar(args_pairs...)
(:a => 1, :b => 2)

julia> bar(args_tuples...)
((:a, 1), (:b, 2))

This is why (; args_tuples...) works and Dict(args_tuples...) does not work. The former splats the tuples into keyword arguments and the latter splats the tuples into positional arguments.

1 Like