Subtyping FieldVector from StaticArrays.jl

I am using this approach in a couple of projects:

julia> import StaticArrays: FieldVector, Size, similar_type

julia> struct Direction{T<:AbstractFloat} <: FieldVector{3, T}
           x::T
           y::T
           z::T
       end

julia> similar_type(::Type{<:Direction}, ::Type{T}, s::Size{(3,)}) where {T} = Direction{T}
similar_type (generic function with 21 methods)

julia> Direction(1.0, 0, 0)
3-element Direction{Float64} with indices SOneTo(3):
 1.0
 0.0
 0.0

julia> Direction(1.0, 0, 0) / 2
3-element Direction{Float64} with indices SOneTo(3):
 0.5
 0.0
 0.0

where I want to enforce that the type is a subtype of AbstractFloat. Everything is fine so far, the type is parametric and the parameter type can be propagated to other types which uses this type as field type.

The problem I encounter with this approach is on the user side, namely the error message which is confusing:

julia> Direction(1, 0, 0)
ERROR: ArgumentError: cannot construct a value of type Union{} for return result
Stacktrace:
 [1] (::Core.TypeofBottom)(a::Tuple{Int64, Int64, Int64})
   @ Core ./boot.jl:318
 [2] Direction(::Int64, ::Vararg{Int64})
   @ StaticArrays ~/.julia/packages/StaticArrays/2dZx4/src/convert.jl:173
 [3] top-level scope
   @ REPL[11]:1

I first thought I can easily solve this with a generic method like:

julia> Direction(_, _, _) = throw(ArgumentError("All elements must be convertible to an AbstractFloat"))
Direction

julia> Direction(1, 0, 0)
ERROR: ArgumentError: All elements must be convertible to an AbstractFloat
Stacktrace:
 [1] Direction(::Int64, ::Int64, ::Int64)
   @ Main ./REPL[19]:1
 [2] top-level scope
   @ REPL[20]:1

julia> Direction(1.0, 0, 0)
ERROR: ArgumentError: All elements must be convertible to an AbstractFloat
Stacktrace:
 [1] Direction(::Float64, ::Int64, ::Int64)
   @ Main ./REPL[19]:1
 [2] top-level scope
   @ REPL[21]:1

but as seen above, it ignores the (in my view) more specific method in this case.

I also tried to replace the inner constructor and check the types there, which almost works, but as seen below, now the parametric constructor is gone :laughing: So when I use already existing objects, the “type propagation” fails

julia> struct Direction{T<:AbstractFloat} <: FieldVector{3, T}
           x::T
           y::T
           z::T
           function Direction(x, y, z)
               T = promote_type(typeof(x), typeof(y), typeof(z))
               if !(T <: AbstractFloat)
                   throw(ArgumentError("All elements must be convertible to an AbstractFloat"))
               end
               new{T}(x, y, z)
           end
       end

julia> similar_type(::Type{<:Direction}, ::Type{T}, s::Size{(3,)}) where {T} = Direction{T}
similar_type (generic function with 21 methods)

julia> Direction(1, 0, 0)
ERROR: ArgumentError: All elements must be convertible to an AbstractFloat
Stacktrace:
 [1] Direction(x::Int64, y::Int64, z::Int64)
   @ Main ./REPL[2]:8
 [2] top-level scope
   @ REPL[3]:1

julia> Direction(1.0, 0, 0)
3-element Direction{Float64} with indices SOneTo(3):
 1.0
 0.0
 0.0

julia> Direction(1.0, 0, 0) / 2
ERROR: The constructor for Direction{Float64}(::Float64, ::Float64, ::Float64) is missing!
Stacktrace:
  [1] error(s::String)
    @ Base ./error.jl:35
  [2] _missing_fa_constructor(FA::Any, AT::Any)
    @ StaticArrays ~/.julia/packages/StaticArrays/2dZx4/src/FieldArray.jl:13
  [3] construct_type(::Type{Direction{Float64}}, x::StaticArrays.Args{Tuple{Float64, Float64, Float64}})
    @ StaticArrays ~/.julia/packages/StaticArrays/2dZx4/src/FieldArray.jl:8
  [4] StaticArray
    @ ~/.julia/packages/StaticArrays/2dZx4/src/convert.jl:173 [inlined]
  [5] FieldArray
    @ ~/.julia/packages/StaticArrays/2dZx4/src/FieldArray.jl:2 [inlined]
  [6] macro expansion
    @ ~/.julia/packages/StaticArrays/2dZx4/src/mapreduce.jl:78 [inlined]
  [7] _map
    @ ~/.julia/packages/StaticArrays/2dZx4/src/mapreduce.jl:42 [inlined]
  [8] map
    @ ~/.julia/packages/StaticArrays/2dZx4/src/mapreduce.jl:33 [inlined]
  [9] /(a::Direction{Float64}, b::Int64)
    @ StaticArrays ~/.julia/packages/StaticArrays/2dZx4/src/linalg.jl:24
 [10] top-level scope
    @ REPL[5]:1

I am not sure how to solve this in an elegant way and I am currently stuck. I read through a couple of StaticArray topics but none of my endeavours were successful.

Maybe I am overcomplicating things and cat’t see the forest for the trees :wink:

I think I solved with this:

struct Direction{T<:AbstractFloat} <: FieldVector{3, T}
    x::T
    y::T
    z::T
    function Direction{U}(x, y, z) where U
        T = promote_type(typeof(x), typeof(y), typeof(z), U)
        if !(T <: AbstractFloat)
            throw(ArgumentError("All elements must be convertible to an AbstractFloat"))
        end
        new{T}(x, y, z)
    end
end
similar_type(::Type{<:Direction}, ::Type{T}, s::Size{(3,)}) where {T} = Direction{T}

not sure if it’s the best way though. Also not sure what kind of downstream errors this will produce :wink:

Edit: to early, now this does not work anymore:

julia> Direction(1, 2, 3)
ERROR: ArgumentError: cannot construct a value of type Union{} for return result
Stacktrace:
 [1] (::Core.TypeofBottom)(a::Tuple{Int64, Int64, Int64})
   @ Core ./boot.jl:318
 [2] Direction(::Int64, ::Vararg{Int64})
   @ StaticArrays ~/.julia/packages/StaticArrays/2dZx4/src/convert.jl:173
 [3] top-level scope
   @ REPL[4]:1

OK this is it (not really happy because there is no explicit conversion for e.g. only integer type parameters, but at least the error makes a bit more sense now). As usual, I was overthinking it…

struct Direction{T<:AbstractFloat} <: FieldVector{3, T}
    x::T
    y::T
    z::T
end
function Direction(x, y, z)
    T = promote_type(typeof(x), typeof(y), typeof(z))
    if !(T <: AbstractFloat)
       throw(ArgumentError("All elements must be convertible to an AbstractFloat"))
    end
    Direction{T}(x, y, z)
end
similar_type(::Type{<:Direction}, ::Type{T}, s::Size{(3,)}) where {T} = Direction{T}