How can I create type stable stable tuples with non-concrete parameters (or: Invariant tuple types)

Hey all,

So, I made this script where I have a type like this:

struct Foo
    v::NTuple{8, Union{String, Nothing}}
end

The reason being that I want the compiler to be able to enforce a length of 8 for me, and also that a an object more lightweight than an array wouldn’t be bad, either.

However, code involving Foo is inherently type unstable because:

julia> isconcretetype(NTuple{8, Union{String, Nothing}})
false

Which is due to tuples being co-variant in their type parameters.

Is there any way for me to use tuples - or something like tuples - such that they can be concretely typed, while also containing Union elements?

Here are the approaches I’ve tried

  • Use an SVector from StaticArrays.jl. Doesn’t work - since StaticArrays implements SVector with tuples, it’s still type unstable
  • Just use a vector, then check during instantiation that the length of the vector is 8. Doable, but not that nice - for example, the compiler cannot tell me if I accidentally mess up the lengths, and cannot automatically elide bounds checks etc.
  • Wrap the union in a struct Bar, then have Foo contain NTuple{8, Bar}. This is the solution I ended up going with (using ErrorTypes.jl), and it does work, but I’m wondering if I missed anything simpler.

Perhaps dumb suggestion, but if length(v) is always 8, perhaps your type Foo could basically implement a Tuple of length 8? Eg.

struct Foo{8 type parameters}
    v1
    v2
    etc...
end

?

2 Likes

I think you can’t get more type stable than this:

julia> struct Foo3{T<:NTuple{8,Union{String,Nothing}}}
           v::T
       end

julia> x = Foo3(ntuple(i -> i < 4 ? "a" : nothing, 8))
Foo3{Tuple{String, String, String, Nothing, Nothing, Nothing, Nothing, Nothing}}(("a", "a", "a", nothing, nothing, nothing, nothing, nothing))

julia> x = Foo3(ntuple(i -> i < 4 ? "a" : nothing, 7))
ERROR: MethodError: no method matching Foo3(::Tuple{String, String, String, Nothing, Nothing, Nothing, Nothing})
Closest candidates are:
  Foo3(::T) where T<:NTuple{8, Union{Nothing, String}} at REPL[7]:2
Stacktrace:
 [1] top-level scope
   @ REPL[9]:1


but that will put quite a price on the compiler, as all objects will have different types, dispatch on that will be probably a mess.

To be truth, I’m not sure this has any advantage over the original proposal, unless you the positions of the nothings and strings are the same on every object at the end, in which case functions could specialize to that exact structure. Otherwise anything operating on these or those objects will end up doing run-time dispatch anyway.

1 Like

That would probably be even worse, since it not only have to do runtime dispatch, but also compile new methods for every variant of the tuple.

2 Likes

Wouldn’t that happen to anything you do with those tuples at the end? Seems to me that it is hard do not end up with a 2^8 dispatch table somewhere, by using any of these approaches.

No, using the struct approach, the tuple itself is type stable, and the only instability is when the Union{Nothing, String} is accessed, in which case union-splitting will take care of it.

1 Like