Help with introspection on types

This question arises from my attempt to write new constructors for DataStructures.jl. Please see the trace below (1.7.1), which raises the following questions:

  1. Is there any difference between <type>.parameters and <type>.types? They gave the same answers in every test below.

  2. Why did collect return different answers when applied to a tuple versus an array in Tests 3 and 4 and yet there was no difference in Tests 1 and 2?

  3. (Main question.) In Test #4, how can I extract the two parameters of the type? I would have expected the two parameters to be Int and Any. Both .parameters and .types failed.

julia> # Test 1

julia> c = collect([(1,3),(2,"a")]);

julia> eltype(c)
Tuple{Int64, Any}

julia> eltype(c).types
svec(Int64, Any)

julia> eltype(c).parameters
svec(Int64, Any)

julia> # Test 2

julia> c = collect(((1,3), (2, "a")));

julia> eltype(c)
Tuple{Int64, Any}

julia> eltype(c).types
svec(Int64, Any)

julia> eltype(c).parameters
svec(Int64, Any)

julia> # Test 3

julia> c = collect([1=>3, 2=>"a"])
2-element Vector{Pair{Int64, Any}}:
 1 => 3
 2 => "a"

julia> eltype(c)
Pair{Int64, Any}

julia> eltype(c).types
svec(Int64, Any)

julia> eltype(c).parameters
svec(Int64, Any)

julia> # Test 4

julia> c = collect((1=>3, 2=>"a"));

julia> eltype(c)
Pair{Int64}

julia> eltype(c).types
ERROR: type UnionAll has no field types
Stacktrace:
 [1] getproperty(x::Type, f::Symbol)
   @ Base .\Base.jl:37
 [2] top-level scope
   @ REPL[81]:1

julia> eltype(c).parameters
ERROR: type UnionAll has no field parameters
Stacktrace:
 [1] getproperty(x::Type, f::Symbol)
   @ Base .\Base.jl:37
 [2] top-level scope
   @ REPL[82]:1
1 Like

By tracking down the code, [1=>3, 2=>"a"] calls Base.vect(1=>3, 2=>"a"), which in turn calls promote_typeof (base of promote). The type Vector{Pair{Int,Any}} is already determined at this point.
(1=>3, 2=>"a") is a tuple type, so the promotion is delayed until collect, which calls promote_typejoin (base of typejoin), which returns Pair{Int} (shorthand for Pair{Int, T} where T). These are two different types: Pair{Int,Any} is a single type, while Pair{Int} is a set of types: Pair{Int, Int}, Pair{Int, String} or even Pair{Int, Any}.

The different code paths promote vs typejoin lead to different result, but I am unclear about the reason though.

Since you see their is no single type, you might need to convert them to a single type maybe with some type calculations.

Regarding Pair{Int}: this appears to be some kind of gap in the Julia type system. As far as I can see, there is no difference between Vector{Pair{Int,T} where T} and Vector{Pair{Int,Any}}, except that the former is less amenable to type introspection. Note that Julia automatically interprets Vector{T where T} as Vector{Any}.

Regarding my main problem of inferring the data type of an arbitrary iterable of pairs: below is the constructor I am planning to use. This constructor has the big disadvantage that it copies the data three times. When the type parameters are known, the data is copied just once. (This disadvantage will be documented.) I use the same pattern for SortedMultiDict. Better solutions are welcome.

function SortedDict(o::Ordering, kv)
    c = collect(kv)
    if eltype(c) <: Pair
        c2 = collect((t.first, t.second) for t in c)
    elseif eltype(c) <: Tuple
        c2 = collect((t[1], t[2]) for t in c)
    else
        throw(ArgumentError("In SortedDict(o,kv), kv should contain either pairs or 2-tuples"))
    end
    SortedDict{eltype(c2).parameters[1], eltype(c2).parameters[2], typeof(o)}(o, c2)
end