Extract unique types between two Tuples at compile time

Hello,

I have two Tuples x and y containing objects of different types. I now want to create at compile time a list containing only the unique types. I have tried this

function test2(x::T1, y::T2) where {T1 <: Tuple, T2 <: Tuple}
    all_types = (T1.parameters..., T2.parameters...)
    return unique(all_types)
end

x = (1, 2, 3 + 1im)
y = (4, 5.0)

test2(x, y)

which however returns a Vector, so the length is not known, as well as the list of the types.

Is there a way to do it without using the @generated macro?

For example, if I have two NamedTuples, the merge function does something similar to the keys, as they need to be merged.

This was a fun exercise. I took inspiration from your comment about merging NamedTuples and came up with the following. Not sure if it handles every case, but it works for the x and y you gave.

@inline function symbolize(T)
    if isempty(T.parameters)
        return T.name.name
    else
        params = ntuple(i -> symbolize(T.parameters[i]), length(T.parameters))
        return Symbol(T.name.name, "{", params..., "}")
    end
end

@inline function make_nt(T)
    name = (symbolize(T),)
    NamedTuple{name}(name)
end

function get_unique_types(x::T1, y::T2) where {T1 <: Tuple, T2 <: Tuple}
    nts1 = ntuple(i -> make_nt(T1.parameters[i]), length(T1.parameters))
    nts2 = ntuple(i -> make_nt(T2.parameters[i]), length(T2.parameters))
    ntc = merge(nts1..., nts2...)
    return keys(ntc)
end
`@code_warntype` output
julia> x = (1, 2, 3 + 1im); y = (4, 5.0);

julia> get_unique_types(x, y)
(:Int64, Symbol("Complex{Int64}"), :Float64)

julia> @code_warntype get_unique_types(x, y)
MethodInstance for get_unique_types(::Tuple{Int64, Int64, Complex{Int64}}, ::Tuple{Int64, Float64})
  from get_unique_types(x::T1, y::T2) where {T1<:Tuple, T2<:Tuple} @ Main REPL[1]:15
Static Parameters
  T1 = Tuple{Int64, Int64, Complex{Int64}}
  T2 = Tuple{Int64, Float64}
Arguments
  #self#::Core.Const(Main.get_unique_types)
  x::Tuple{Int64, Int64, Complex{Int64}}
  y::Tuple{Int64, Float64}
Locals
  #3::var"#get_unique_types##2#get_unique_types##3"{Tuple{Int64, Float64}}
  #2::var"#get_unique_types##0#get_unique_types##1"{Tuple{Int64, Int64, Complex{Int64}}}
  ntc::@NamedTuple{Int64::Symbol, var"Complex{Int64}"::Symbol, Float64::Symbol}
  nts2::Tuple{@NamedTuple{Int64::Symbol}, @NamedTuple{Float64::Symbol}}
  nts1::Tuple{@NamedTuple{Int64::Symbol}, @NamedTuple{Int64::Symbol}, @NamedTuple{var"Complex{Int64}"::Symbol}}
Body::Tuple{Symbol, Symbol, Symbol}
1 ─ %1  = Main.ntuple::Core.Const(ntuple)
│   %2  = Main.:(var"#get_unique_types##0#get_unique_types##1")::Core.Const(var"#get_unique_types##0#get_unique_types##1")
│   %3  = $(Expr(:static_parameter, 1))::Core.Const(Tuple{Int64, Int64, Complex{Int64}})
│   %4  = Core.apply_type(%2, %3)::Core.Const(var"#get_unique_types##0#get_unique_types##1"{Tuple{Int64, Int64, Complex{Int64}}})
│         (#2 = %new(%4))
│   %6  = #2::Core.Const(var"#get_unique_types##0#get_unique_types##1"{Tuple{Int64, Int64, Complex{Int64}}}())
│   %7  = Main.length::Core.Const(length)
│   %8  = $(Expr(:static_parameter, 1))::Core.Const(Tuple{Int64, Int64, Complex{Int64}})
│   %9  = Base.getproperty(%8, :parameters)::Core.Const(svec(Int64, Int64, Complex{Int64}))
│   %10 = (%7)(%9)::Core.Const(3)
│         (nts1 = (%1)(%6, %10))
│   %12 = Main.ntuple::Core.Const(ntuple)
│   %13 = Main.:(var"#get_unique_types##2#get_unique_types##3")::Core.Const(var"#get_unique_types##2#get_unique_types##3")
│   %14 = $(Expr(:static_parameter, 2))::Core.Const(Tuple{Int64, Float64})
│   %15 = Core.apply_type(%13, %14)::Core.Const(var"#get_unique_types##2#get_unique_types##3"{Tuple{Int64, Float64}})
│         (#3 = %new(%15))
│   %17 = #3::Core.Const(var"#get_unique_types##2#get_unique_types##3"{Tuple{Int64, Float64}}())
│   %18 = Main.length::Core.Const(length)
│   %19 = $(Expr(:static_parameter, 2))::Core.Const(Tuple{Int64, Float64})
│   %20 = Base.getproperty(%19, :parameters)::Core.Const(svec(Int64, Float64))
│   %21 = (%18)(%20)::Core.Const(2)
│         (nts2 = (%12)(%17, %21))
│   %23 = Main.merge::Core.Const(merge)
│   %24 = nts1::Core.Const(((Int64 = :Int64,), (Int64 = :Int64,), (var"Complex{Int64}" = Symbol("Complex{Int64}"),)))
│   %25 = nts2::Core.Const(((Int64 = :Int64,), (Float64 = :Float64,)))
│         (ntc = Core._apply_iterate(Base.iterate, %23, %24, %25))
│   %27 = Main.keys::Core.Const(keys)
│   %28 = ntc::Core.Const((Int64 = :Int64, var"Complex{Int64}" = Symbol("Complex{Int64}"), Float64 = :Float64))
│   %29 = (%27)(%28)::Core.Const((:Int64, Symbol("Complex{Int64}"), :Float64))
└──       return %29

EDIT: This code outputs Symbols, not types as requested in the OP. See my later comment for slight changes to return types instead.

1 Like

Thanks! It works.

I’m glad it works! Though I just realized that the function I gave you gives you Symbols, not the types themselves. If you want the types themselves, define make_nt as:

@inline make_nt(T) = NamedTuple{(symbolize(T),)}((T,))

And then in get_unique_types:

return values(ntc)

for acessing the types of a tuple (or any other struct), the way to do it without using the fields of DataType is by using the function fieldtypes

julia> m = (1.0,2,"a")
(1.0, 2, "a")

julia> fieldtypes(typeof(m))
(Float64, Int64, String)
1 Like