How to convert element type of a container?

Suppose I have a container, a, could be a Tuple, a Vector, or a user-defined subtype of AbstractArray. Now I want to convert every element of a to another type, e.g., from Ints to Float64. For example, I write a rough function:

convert_eltype(T::Type, a) = map(x -> convert(T, x), a)

This function works fine with Tuple and Vector, but not the last one.

julia> using StaticArrays

julia> struct A{T} <: FieldVector{2, T}
             x::T
             y::T
         end

julia> convert_eltype(Float64, A(1, 2))
2-element SArray{Tuple{2},Float64,1,2}:
 1.0
 2.0

This breaks the container type A:

julia> f(::A) = 1
f (generic function with 1 method)

julia> f(convert_eltype(Float64, A(1, 2)))
ERROR: MethodError: no method matching f(::SArray{Tuple{2},Float64,1,2})
Closest candidates are:
  f(::A) at REPL[7]:1
Stacktrace:
 [1] top-level scope at none:0

I also tried other ways:

convert_eltype(T::Type, a) = typeof(a)(map(x -> convert(T, x), a))

But it does not work. typeof(a) contains the Int information which makes map(x -> convert(T, x), a) work in vain.
I try to strip the container type but am told it seems not possible. I do not find similar useful because it just constructs a new uninitialized container, not converting one type to another.

Define a StaticArrays.similar_type method for your FieldVector. See Is there a way to seamlessly inherit vector arithmetic on new types? - #9 by tkoolen.

2 Likes

That’s a good way of doing this. I have read the doc of similar_type, and I found that this only works with concrete type AA:

julia> using StaticArrays

julia> import StaticArrays: similar_type

julia> export similar_type

julia> struct AA{T} <: FieldVector{2, T}
           x::T
           y::T
       end

julia> convert_eltype(T::Type, a) = map(x -> convert(T, x), a)
convert_eltype (generic function with 1 method)

julia> similar_type(::Type{A}, ::Type{T}, size::Size) where {A <: AA, T} = AA{T}
similar_type (generic function with 18 methods)

julia> similar_type(AA, Float64, Size(2))
AA{Float64}

julia> convert_eltype(Float64, AA(1, 2))
2-element AA{Float64}:
 1.0
 2.0

But if AA is an abstract type and it has several subtypes, this will break:

julia> abstract type M{N, T} <: FieldVector{N, T} end

julia> struct AA{T} <: M{2, T}
           x::T
           y::T
       end

julia> convert_eltype(T::Type, a) = map(x -> convert(T, x), a)
convert_eltype (generic function with 1 method)

julia> similar_type(::Type{A}, ::Type{T}, size::Size) where {A <: M, T} = A{T}
similar_type (generic function with 18 methods)

julia> similar_type(AA, Float64, Size(2))
AA{Float64}

julia> convert_eltype(Float64, AA(1, 2))
ERROR: TypeError: in Type{...} expression, expected UnionAll, got Type{AA{Int64}}
Stacktrace:
 [1] similar_type(::Type{AA{Int64}}, ::Type{Float64}, ::Size{(2,)}) at ./REPL[7]:1
 [2] macro expansion at /Users/qz/.julia/packages/StaticArrays/mcf7t/src/mapreduce.jl:30 [inlined]
 [3] _map(::getfield(Main, Symbol("##3#4")){DataType}, ::Size{(2,)}, ::AA{Int64}) at /Users/qz/.julia/packages/StaticArrays/mcf7t/src/mapreduce.jl:21
 [4] map at /Users/qz/.julia/packages/StaticArrays/mcf7t/src/mapreduce.jl:11 [inlined]
 [5] convert_eltype(::Type, ::AA{Int64}) at ./REPL[6]:1
 [6] top-level scope at none:0

It does not seem to be my problem: The error happens in StaticArrays.jl. So the only way is that I add a method for each subtype of M?

This is basically the same question you asked in How to get the container type of a container?. You’ll have to define a similar_type overload for each concrete FieldVector subtype you define. If that’s too much of a hassle, you could write a macro to generate the similar_type method along with the FieldVector subtype definition.

Note by the way that you’ll want to be more specific with the Size argument:

StaticArrays.similar_type(::Type{<:AA}, ::Type{T}, ::Size{(2,)}) where {T} = AA{T}

otherwise,

julia> aa = AA(1, 2)
2-element AA{Int64}:
 1
 2

julia> [aa; aa]
ERROR: DimensionMismatch("No precise constructor for AA{Int64} found. Length of input was 4.")
Stacktrace:
 [1] AA{Int64}(::Tuple{Tuple{Tuple{NTuple{4,Int64}}}}) at /Users/tkoolen/.julia/packages/StaticArrays/mcf7t/src/convert.jl:1
 [2] Type at /Users/tkoolen/.julia/packages/StaticArrays/mcf7t/src/convert.jl:4 [inlined] (repeats 3 times)
 [3] macro expansion at /Users/tkoolen/.julia/packages/StaticArrays/mcf7t/src/linalg.jl:114 [inlined]
 [4] _vcat at /Users/tkoolen/.julia/packages/StaticArrays/mcf7t/src/linalg.jl:96 [inlined]
 [5] vcat(::AA{Int64}, ::AA{Int64}) at /Users/tkoolen/.julia/packages/StaticArrays/mcf7t/src/linalg.jl:92
 [6] top-level scope at none:0
1 Like

That’s indeed why I asked this question. I am surprised that Julia does not provide a simple way of doing this.

The key thing is that Julia doesn’t enforce that the first type parameter of A here is the same type parameter that appears in M{_, T}. In fact, another person could come along and define a

      struct BB{T, S, R} <: M{2, R}
           x::R
           y::R
           bonus::T
           more::S 
       end

Now your generic similar_type method is no longer correct.

4 Likes