Tuple and NamedTuple types

Dear all,

I have some trouble to infer the types of Tuples and NamedTuples. Let’s start with a simple tuple. If each field in the tuple is of the same type, I can state the type of the tuple as follows (MWE):

a = (:b, :c, :d)
typeof(a) #Tuple{Symbol, Symbol, Symbol}
typeof(a) <: NTuple{k, Symbol} where k #true
  1. Question

Let’s say now that I define a custom struct and I want that the tuple can only hold the custom structs, but the struct itself has parametric types. How can I force the tuple to only hold such custom structs? MWE:

struct Test1{A, B}
	a	::	A
	b 	::	B
end

b = (Test1("1", :b), Test1(3., 4), )
typeof(b) #Tuple{Test1{String, Symbol}, Test1{Float64, Int64}}
typeof(b) <: NTuple{k, T} where {k, T<:Test1} #false

This would be useful to me if I want to have a field of another struct to only hold tuples of Test1 types. I can already do something more general, but
any tuple could be inserted, which is not what I want.

struct BigStruct{A<:Tuple}
	a 	:: 	A
end
BigStruct(b) #working

I would like to force that field a can only hold Tuples of Test1 types:

struct SafeBigStruct{k, T<:Test1}
	a 	:: 	NTuple{k, T}
end
SafeBigStruct(b) # NOT working -> no method matching SafeBigStruct...
  1. Question

Extending this to NamedTuples, how would this work? As far as i understand, NamedTuple types contain the types of its keys and values:

c = (d = Test1("1", :d), e = Test1(3., 4), )
typeof(c) #NamedTuple{(:d, :e), Tuple{Test1{String, Symbol}, Test1{Float64, Int64}}}

The key values should always have type NTuple{k, Symbol} where k, but just as in the previous question, I cannot infer the value types. Is there any way to get the NamedTuple type, such that it still enforces that all values have to be of type Test1?

Question 1: the type NTuple{k, T} where {k, T<:Test1} doesn’t work because that defines a tuple type where all values are the same subtype of Test1. Indeed it works if you have Test1 instances of the same concrete type:

julia> (Test1("1", :b), Test1("2", :c)) isa NTuple{k, T} where {k, T<:Test1}
true

(Note that instead of typeof(x) <: T I used the simpler form x isa T.)

What you want is still easy to achieve because tuples (unlike other types in Julia) are covariant. For example one has Tuple{Int,Float64} <: Tuple{Number,Number} (no need to write <:Number). Similarly in your case you can use:

julia> b isa NTuple{K, Test1} where K
true

and there’s even a short version for this using Vararg:

julia> b isa Tuple{Vararg{Test1}}
true

Question 2: for named tuples it’s trickier because they are not covariant, but since the types are encoded with a tuple (which is covariant) it’s still easy to do:

julia> c isa NamedTuple{names, <:Tuple{Vararg{Test1}}} where {names}
true
2 Likes

Let’s focus on the first Tuple question first.

struct Test1{A, B}
  a::A
  b::B
end

test1x = Test1("one", Int8(1))  
# Test1{String, Int8}("one", 1)
test1y = Test1('1', UInt16(1))  
# Test1{Char, UInt16}('1', 0x0001)
test1 = (test1x, test1y)
# ( Test1{String, Int8}("one", 1), 
#    Test1{Char, UInt16}('1', 0x0001) )

typeof(test1) 
# Tuple{Test1{String, Int8}, Test1{Char, UInt16}}

typeof(test1) <: Tuple{Vararg{Test1, N}} where {N}
# true

The use of this type constraining pattern

  • Tuple{ Vararg{T,N} } where {T,N}
    is more flexible than
  • NTuple{N,T} where {N,T}

notice that the order of the parameters in Vararg{T,N} is swapped with respect to the order in NTuple{T,N}
I would like to see that changed

wrapped_typename(::Type{T}) where {T} = T.name.wrapper
wrapped_typename(x::T) where {T} = wrapped_typename(T)
wrapped_typename(x::UnionAll} = x

unwrapped_typename(::Type{T}) where {T} = T
unwrapped_typename(x::T) where {T} = T

wrapped_unwrapped(::Type{T}) where {T} =
  (wrapped_typename(T), unwrapped_typename(T))
wrapped_unwrapped(x::T) where {T} = wrapped_unwrapped(T)
julia> wrapped_unwrapped(Int16)
(Int16, Int16)

julia> wrapped_unwrapped(Rational{Int32})
(Rational, Rational{Int32})

julia> wrapped_unwrapped(Rational{Int64})
(Rational, Rational{Int64})

julia> wrapped_unwrapped(test1x)
(Test1, Test1{String, Int8})

julia> wrapped_unwrapped(test1)
(Tuple, Tuple{Test1{String, Int8}, Test1{Char, UInt16}})

julia> typeof(test1)
Tuple{Test1{String, Int8}, Test1{Char, UInt16}}
1 Like

Thanks a lot for your detailed answer! Just to confirm, Tuple{Vararg{Test1}} is not a concrete type? For instance, the following field type might result in a speed loss:

struct SafeBigStruct2
	a 	:: 	Tuple{Vararg{Test1}}
end
SafeBigStruct2(b)

I can circumvent that (and still enforce the Test1 tuple) by doing the following, though:

struct SafeBigStruct3{K<:Tuple{Vararg{Test1}}}
	a 	:: 	K
end
SafeBigStruct3(b)

Yes I think that’s exactly right, though in this case I would probably go back to NTuple and write

struct SafeBigStruct4{K}
    a :: NTuple{K, Test1}
end

(Edit: that’s not doing what is desired which is a struct type with concrete field types, see below)

1 Like

Thank you! But BigStruct4 would also not be concrete, right?

SafeBigStruct3(b) #SafeBigStruct33{Tuple{Test1{String, Symbol}, Test1{Float64, Int64}}}
SafeBigStruct4(b) #SafeBigStruct4{2}

Oops sorry you’re right of course!

s3 = SafeBigStruct3(b)
s4 = SafeBigStruct4(b)

julia> isconcretetype(fieldtype(typeof(s3), :a))
true

julia> isconcretetype(fieldtype(typeof(s4), :a))
false