How to get the container type of a container?

For example, I have an x = Vector{Int}, then do what operations on x can I get the Vector ?

1 Like

If you want to get Array from Vector{Int}, I’ve been using

constructor_of(::Type{T}) where T =
    getfield(T.name.module, Symbol(T.name.name))

since Julia 0.6 for this purpose. If you only target 1.x, I think

constructor_of(::Type{T}) where T =
    getfield(parentmodule(T), Symbol(Base.typename(T)))

may be better.

But getting Vector from Vector{Int} seems harder because Vector{Int} actually is Array{Int,1}.

If you need to special-case only a few types, you can add a method to constructor_of:

constructor_of(::Type{T}) where {T <: Vector} = Vector
1 Like

I’m not sure exactly what you’re asking here.

Do you have a vector instance (eg, x = [1,2,3]) and you want to get the type? You can use typeof(x) for that.

Do you have a vector instance x and want to create another vector y which is the same shape and type? Use similar(x) for that.

1 Like

Sorry, I should be more clear. I am writing a pseudo-code here because it is not a valid syntax in Julia:

julia> f(x::T{Int}) where {T <: AbstractArray} = convert(T{Float64}, x)
ERROR: TypeError: in Type{...} expression, expected UnionAll, got TypeVar

i.e., f eats an AbstractArray of something (Int here), and converts it to an AbstractArray of another thing (Float64 here). However, Julia does not allow TypeVar in the function definition. I know I can use map in such simple case, but what I am actually facing is a bit more complicated that this example so I feel hard to write map.

I am not sure it would work in general, since convert does not need to be defined for a type to implement the AbstractArray interface. Eg

struct MyArray{T, N, A <: AbstractArray{T,N}} <: AbstractArray{T,N}
    parent::A
end

Base.size(A::MyArray) = size(A.parent)
Base.getindex(A::MyArray, I...) = getindex(A.parent, I...)

A = rand(Int,3,3)
B = MyArray(A)
convert(MyArray{Float64}, B)    # will error

If I understand correctly, you want to strip the type parameters from a type. That is not possible in general Stripping parameter from parametric types. similar is made for this.

4 Likes

Yes, you are right. That’s what I want!

I’ve landed on this thread many, many times so I’ll just paste my modified solution here. The above version was not general enough for me, since I wanted to get the container of an arbitrary user-defined type, not just AbstractArrays.

I don’t remember where I saw this recursive solution originally (I think maybe the Julia docs, evolved from this post?), but here is the code I’ve been using, with some modifications to make it instantaneous

function _container_type(T::Type)
    isa(T, UnionAll) && return _container_type(T.body)
    return T.name.wrapper
end

@generated function container_type(::Type{T}) where {T}
    container = _container_type(T)
    return :($container)
end

The reason it’s inside a @generated is just for caching. You can’t do (::Type{T}) where {T} in the _container_type because Julia will hit a stack overflow.

With this, you can get the container for arbitrary types, with no runtime cost:

julia> container_type(AbstractArray)
AbstractArray

julia> container_type(AbstractArray{Float64,3})
AbstractArray

julia> struct F{T1,T2,T3}
           x::T1
           y::T2
           z::T3
       end

julia> container_type(F{Float64,String,Tuple{Float64,Float32}})
F

julia> @btime container_type(F{Float64,String})
  0.875 ns (0 allocations: 0 bytes)
F

However, this will breaks if there is some abstract type with more type parameters than the user-defined struct.

Are the fields of DataType objects part of the public interface of Julia?

Doesn’t seem like there’s an alternative:

Actually, I guess this is a bit cleaner?

function _container_type(T::Type)
    return Base.typename(T).wrapper
end

@generated function container_type(::Type{T}) where {T}
    container = _container_type(T)
    return :($container)
end

But .wrapper is still needed

Oops, now the @generated isn’t even needed.

function container_type(::Type{T}) where {T}
    return Base.typename(T).wrapper
end
1 Like

For comparison, ConstructionBase uses getfield(parentmodule(T), nameof(T)) for this purpose: ConstructionBase.jl/src/ConstructionBase.jl at master · JuliaObjects/ConstructionBase.jl · GitHub.

1 Like

Thanks!

I have no idea how to evaluate these because they both compiled to the same LLVM…

julia> function container_type(::Type{T}) where {T}
           return Base.typename(T).wrapper
       end;

julia> @generated function constructorof(::Type{T}) where T
           getfield(parentmodule(T), nameof(T))
       end

which gives

julia> @code_llvm container_type(F{Float64,Float32})
;  @ REPL[12]:1 within `container_type`
define nonnull {}* @julia_container_type_478({}* readonly %0) #0 {
top:
;  @ REPL[12]:2 within `container_type`
  ret {}* inttoptr (i64 4534794992 to {}*)
}

julia> @code_llvm constructorof(F{Float64,Float32})
;  @ REPL[11]:1 within `constructorof`
define nonnull {}* @julia_constructorof_475({}* readonly %0) #0 {
top:
; ┌ @ REPL[11]:1 within `macro expansion`
   ret {}* inttoptr (i64 4534794992 to {}*)
; └
}

The downside of constructorof is that uses a generated function, and the downside of container_type is that it doesn’t use the public interface… sigh…

1 Like

I’ll note that Polynomials.jl, for example, also uses the Base.typename(T).wrapper solution:

However, the Julia Manual actually addresses this question, and the recommended solution is completely different, and much nicer IMO:

https://docs.julialang.org/en/v1/manual/methods/#Building-a-similar-type-with-a-different-type-parameter

So: create a function, and then have each relevant type implement a method of the function. This requires some thinking about interface design, though.

1 Like

Thanks for pointing this out; it’s good to know that type of approach is at least robust enough to be used in larger packages.

I suppose, but then again, I would think the main motivation for automatically extracting a container type is to avoid burdening the user with a method they need to implement. (At least this was true for my case). This is especially true if the methods are further upstream in the dependency tree.

I guess the strategy imposed by ConstructionBase.jl satisfies both of these desires? Because it (1) by default just returns the container type (which is good enough 99% of the time), but (2) enables the user to write a new constructorof method for any objects that require a weird constructor:

(From Home · ConstructionBase.jl)

(Though in this example I don’t follow why the user wouldn’t want to simply define S(a, b, checksum) = ... at the source)

1 Like

Only on recent Julia, while for example on 1.6 only the parentmodule... solution is zero-cost.
I guess that’s why ConstructionBase uses it, otherwise it would make sense to get rid of this @generated function.