Construct Tuple containing NamedTuple from Tuple

I’m trying to construct a Tuple containing a member NamedTuple from a Tuple. I believe this should work but it doesn’t.

julia> T = Tuple{Int64, Int64, NamedTuple{(:d, :e), Tuple{Int64, Int64}}};

julia> vals = (1,2,(3,4));

julia> T(vals)
ERROR: MethodError: Cannot `convert` an object of type Tuple{Int64, Int64} to an object of type NamedTuple{(:d, :e), Tuple{Int64, Int64}}

Closest candidates are:
  convert(::Type{NamedTuple{names, T}}, ::NamedTuple{names, T}) where {names, T<:Tuple}
   @ Base namedtuple.jl:148
  convert(::Type{NamedTuple{names, T}}, ::NamedTuple{names}) where {names, T<:Tuple}
   @ Base namedtuple.jl:151
  convert(::Type{T}, ::T) where T
   @ Base Base.jl:64
  ...

Stacktrace:
 [1] cvt1
   @ ./essentials.jl:418 [inlined]
 [2] ntuple
   @ ./ntuple.jl:50 [inlined]
 [3] convert(#unused#::Type{Tuple{Int64, Int64, NamedTuple{(:d, :e), Tuple{Int64, Int64}}}}, x::Tuple{Int64, Int64, Tuple{Int64, Int64}})
   @ Base ./essentials.jl:419
 [4] Tuple{Int64, Int64, NamedTuple{(:d, :e), Tuple{Int64, Int64}}}(x::Tuple{Int64, Int64, Tuple{Int64, Int64}})
   @ Base ./tuple.jl:364
 [5] top-level scope
   @ REPL[5]:1

There seem to be two solutions that I can see:

  1. Modify the Tuple constructor to construct each element explicitly:
(::Type{T})(x::Tuple) where {T<:Tuple} = tuple((type(arg) for (type, arg) in zip(T.parameters, x))...)
  1. Implement Base.convert for NamedTuples:
Base.convert(::Type{NT}, val::Tuple) where {NT<:NamedTuple} = NT(val)

Am I missing something obvious? Will either of these proposed solutions cause problems elsewhere?

Thanks for the feedback!

basically, you’re saying the following two should either both work or both not work:

julia> T = NamedTuple{(:d, :e), Tuple{Int64, Int64}}
NamedTuple{(:d, :e), Tuple{Int64, Int64}}

julia> T((1,2))
(d = 1, e = 2)

julia> convert(T, (1,2))
ERROR: MethodError: Cannot `convert` an object of type Tuple{Int64, Int64} to an object of type NamedTuple{(:d, :e), Tuple{Int64, Int64}}

Yes I believe that’s a nice succinct way to put it. Any thoughts on which direction is correct?

Hey Ben,

what about:

@inline ntconvert(::Type, x) = x
@generated function ntconvert(::Type{T}, x) where {T<:NamedTuple}
    fn = fieldnames(T)
    ft = fieldtypes(T)
    e = Expr(:tuple)
    for (i, (n, t)) in enumerate(zip(fn, ft))
        push!(e.args, Expr(:(=), n, :(ntconvert($t, x[$i]))))
    end
    e
end
julia> const T = NamedTuple{(:a,:b), Tuple{Int,Int}}
         const T2 = NamedTuple{(:a,:b,:c), Tuple{Int,Int,T}}
         b = (1,2,(3,4))
         @benchmark ntconvert(T2, $b)
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
 Range (min … max):  2.915 ns … 45.083 ns  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     3.887 ns              ┊ GC (median):    0.00%
 Time  (mean ± σ):   3.890 ns ±  1.068 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

  ▇▂                     █▄            ▁▂▂▃▃▃▂▁     ▁▁▁      ▂
  ██▃▁▁▁▁▁▁▁▃▁▁▁▁▁▁▃▁▁▃▃▁██▁▄▄▅▅▅▄▇▇▇████████████████████▇█▇ █
  2.92 ns      Histogram: log(frequency) by time     5.31 ns <

 Memory estimate: 0 bytes, allocs estimate: 0.

It does not seem to be a noop though. I don’t exactly know why. I’m not sure.

I made the following PR that should solve my issue. Feedback appreciated.

From the manual:
“since convert can be called implicitly, its methods are restricted to cases that are considered “safe” or “unsurprising””. So it’s not completely surprising that MyNamedTupleType(mytuple) works but no convert(MyNamedTupleType, mytuple).

Maybe the reason this conversion is not implemented is because it assumes an ordering on the fields of the tuple? Curious to see the review of the PR!

I wouldn’t really expect the code from the first post to work, neither for tuples nor for any other type. In general, it corresponds to running

struct SomeType
    a::Int
    b::AnotherType
end

SomeType(1, (2, 3))

and expecting that to be turned into SomeType(1, AnotherType(2, 3)). Doesn’t make much sense, IMO.

@aplavin - Interestingly, construction between Tuple/NamedTuple already works for simple types:

julia> @NamedTuple{a::Int}((1,))
(a = 1,)

julia> Tuple{Int}((a=1,))
(1,)

The problem arises when one of the members needs to be implicitly converted, like the example above.

I think these examples are completely unrelated.

Is creating a named tuple from the elements you give it: the 1 is taken as-is, not transformed in any way.

Same, similar to your first post, you can construct a tuple with a tuple:

julia> T = Tuple{Int64, Int64, Tuple{Int64, Int64}};
julia> vals = (a=1,b=2,c=(3,4));
julia> T(vals)
(1, 2, (3, 4))

Again, here provided values are taken as-is.