Stripping parameter from parametric types

I have writing code that maniuplates containers of arrays of custom types with several parameters, e.g.
SomeArrayType{T, G1, G2, N} <: AbstractArray{T, N}, where the last parameter in SomeArrayType is always the array rank N. The arrays that can be in the container can differ in rank N but otherwise should be the same type, for example Vector{SomeArray{T, G1, G2}} which can also be written Vector{SomeArray{T, G1, G2, N} where N}.

I’d like to create a function arraytype that strips the last (rank) parameter from the type, so that
arraytype(::Type{NewArrayType{T, X, Y, Z, N}}) = NewArrayType{T, X, Y, Z} for any type that might be encountered. How would one write a function that generically strips the last parameter from a type?

1 Like

Not sure if there is an easier way or not, but here is my attempt.

julia> name(T::DataType) = T.name
name (generic function with 1 method)

julia> name(T::UnionAll) = name(T.body)
name (generic function with 2 methods)

julia> datatype(T::UnionAll) = datatype(T.body)
datatype (generic function with 1 method)

julia> datatype(T::DataType) = T
datatype (generic function with 2 methods)

julia> nvars(T::UnionAll) = 1 + nvars(T.body)
nvars (generic function with 1 method)

julia> nvars(T::DataType) = 0
nvars (generic function with 2 methods)

julia> unionall(T) = eval(Symbol(name(T)))
unionall (generic function with 1 method)

julia> parametersless1(T) = datatype(T).parameters[1:end-nvars(T)-1]
parametersless1 (generic function with 1 method)

julia> @generated function f(::Type{T}) where {T}
           any(isa.(T, (DataType, UnionAll))) || throw("Only supports DataType and UnionAll inputs.")
           n = unionall(T)
           n isa DataType && return :($n)
           ps = parametersless1(T)
           return :($n{$(ps...)})
       end
f (generic function with 1 method)

julia> struct T{S1,S2,S3} end

julia> f(T{1,2,3})
T{1,2,S3} where S3

julia> f(T{1,2})
T{1,S2,S3} where S3 where S2

julia> f(T{1})
T

julia> f(T)
T

julia> f(Int)
Int64

You can also make a normal function not a generated one. And you can avoid using eval altogether if you find a way to change a TypeName to a UnionAll in case of parametric types, or to a DataType in case of non-parametric types, eval was the lazy solution.

Here is a prototype for Array:

julia> stripN(::Type{Array{T, N}}) where {T, N} = Array{T, M} where M
stripN (generic function with 1 method)

julia> stripN(Vector{Float64})
Array{Float64,M} where M

should be straightforward to do for a type with more parameters.

3 Likes

I think the key point is:

Right. That is my current solution - to define a new method or each array type that I need. I was wondering whether I could avoid that by mucking around in the type system.

You may not need a special function. Consider the following approach.

struct SomeArrayType{T,G1,G2,N} <: AbstractArray{T,N}
content::AbstractArray{T,N}
g1::G1
g2::G2
end

Constructor that automatically defines the value of N by using ndims method:

SomeArrayType(a::AbstractArray, g1, g2) = SomeArrayType{eltype(a), typeof(g1), typeof(g2), ndims(a)}(a, g1, g2)

The show method requires the size and getindex methods to exist:

Base.size(a::SomeArrayType) = size(a.content)
Base.getindex(a::SomeArrayType, I…) = getindex(a.content, I…)

Now define an instance of SomeArrayType without having to specify N:

Julia> SomeArrayType( [1,2,3], ‘a’, 4.0 )
3-element SomeArrayType{Int64,Char,Float64,1}:
1
2
3

Hey jandehaan, my issue is not dealing with constructors. It’s more of this type of thing:

arrs = [SomeArrayType( [1,2,3], ‘a’, 4.0 ), SomeArrayType( [1,2,3], ‘a’, 4.0 )]

function func(arrs::Vector{A}) where A<:AbstractArray
    arrs = deepcopy(arrs)
    # some computation involving arrs
    newarr = SomeArrayType( [[1,2,3],[1,2,3],[1,2,3]], ‘a’, 4.0 )
    push!(arrs, newarr) # causes error
    # more computations and collecting arrays into arrs
    return arrs
end

The issue is that arrs is of type Vector{SomeArrayType{Int, String, Float, 1}} and I need to add an array to it of a different rank (i.e. type SomeArrayType{Int, String, Float, 2} or for arbitrary ranks.) I can fix it with a call to convert(Vector{SomeArrayType{Int, String, Float, N} where N}, arrs) every time I call the function (which is annoying) or inside the function. But inside the function, it’s not clear how to strip the last parameter from the type of A.

If you care about performance, this will probably throw it out of the window.

1 Like

Try this:

struct SomeArrayType{T,G1,G2,N} <: AbstractArray{T,N} 
    content::AbstractArray{T,N}
    g1::G1
    g2::G2
end

# constructor that automatically defines the value of N
SomeArrayType(a::AbstractArray, g1, g2) = 
    SomeArrayType{eltype(a), typeof(g1), typeof(g2), ndims(a)}(a, g1, g2)

Base.size(a::SomeArrayType) = size(a.content)
Base.getindex(a::SomeArrayType, I...) = size(a.content, I...)


Base.show(io::IO, a::SomeArrayType) = 
    print(io, typeof(a), "(", a.content, ", ", a.g1, ", ", a.g2, ")")

function func(arrs::Vector{A}) where A<:AbstractArray
    arrs = convert(Vector{Base.AbstractArray}, arrs)
    arrs = deepcopy(arrs)
    # some computation involving arrs
    newarr = SomeArrayType( [[1,2,3],[1,2,3],[1,2,3]], 'a', 4.0 )
    push!(arrs, newarr) # causes error
    # more computations and collecting arrays into arrs
    return arrs
end

# now define an instance of SomeArrayType
SomeArrayType([1,2,3], 'a', 4.0)

arrs = [SomeArrayType( [1,2,3], 'a', 4.0 ),  
                      SomeArrayType( [1,2,3], "string", 4.0 )]

func(arrs)

The last statement produces:

3-element Array{AbstractArray,1}:
   SomeArrayType{Int64,Char,Float64,1}([1, 2, 3], a, 4.0)
   SomeArrayType{Int64,String,Float64,1}([1, 2, 3], string, 4.0) 
   SomeArrayType{Array{Int64,1},Char,Float64,1}(
           Array{Int64,1}[[1, 2, 3], [1, 2, 3], [1, 2, 3]], a, 4.0)

No, you can’t. The approved solution is wrong – there’s no relation between the ordering of type parameters of a subtype (in this case, SomeArrayType) and its supertype (AbstractArray). Tamas_Papp’s solution is correct. In generic code, this is usually done by calling the similar function.

However, we can also generalize that solution to abstract types as follows (by adding in <: so that it can select subtypes):

stripN(::Type{<:AbstractArray{T, N}}) where {T, N} = AbstractArray{T, M} where M
2 Likes

May you give an example where my attempted solution above fails to do the desired task?

This is a frequently asked question, so I’ve included my reply in the manual: https://docs.julialang.org/en/latest/manual/methods/#Extracting-the-type-parameter-from-a-super-type-1

3 Likes

Thanks for adding that section. I see the point in:

However, it is not hard to construct cases where this will fail:

struct BitVector <: AbstractArray{Bool, 1}; end

Here we have created a type BitVector which has no parameters, but where the element-type is still fully specified, with T equal to Bool!

But how is this related to stripping the last specified parameter in a parametric type? In this case, BitVector is not parametric in its own right so it will be just returned back. Your comment is probably related to the bigger goal of the OP. I was just curious if my attempted solution could fail to do what I wanted it to do, regardless of whether or not it is the best way to reach the OP’s goal (clearly not from your comment).

1 Like

“stripping the last specified parameter in a parametric type” is an underspecified operation. There’s perhaps several ways of doing it, but it’s slightly arbitrary to care about this super type compared to any other possible super type. As for that, I would probably go with the following implementation, until someone explained why they wanted to do this particular non-sensical operation:

supertypeof(x::T) where {T} =
    (isempty(T.parameters) ? supertype(T) : T.name.wrapper{T.parameters[1:(end - 1)]...})
1 Like

What about stripping all type parameters? So going from Array{T,N} to Array, but generically (for other types besides Array). That is well-defined, right?

1 Like

You can use Base.typename in recent versions of Julia

5 Likes

Base.typename returns a Core.TypeName object. To get the bare type, you still have to use Base.typename(T).wrapper. It seems useing T.name.wrapper is more straightforward.

3 Likes

It seems tacky to use T.name.wrapper. Is this a stable API?

1 Like

I’m hitting this issue so often when dealing with custom arrays that have parameters that I usually just define manually the nonparametric_type

struct Custom1Array{P1, P2, P3} <: AbstractCustomArray
nonparametric_type(::Type{Custom1Array}) = Custom1Array

struct Custom2Array{P1, P2, P3} <: AbstractCustomArray
nonparametric_type(::Type{Custom2Array}) = Custom2Array

and then I can write a function like

function f(A::CustomArray, args...) where {CustomArray<:AbstractCustomArray}
    CustomArray_ = nonparametric_type(CustomArray)
    # now construct new array with parameters P1, P2, P3 depending on args...
    return CustomArray_{P1, P2, P3}(...)
end

for all <:AbstractCustomArray. Adding a nonparametric_type method for a new type is really not a burden compared to the type definition itself and yes T.name.wrapper feels somewhat hacky.

2 Likes