Get tuple length from type?

Is there a built-in function to get the length of a tuple as a Val{N}?

Basically I’m looking for something that would be of the form:

tuplen(::Type{Tuple{}}) = Val{0}()
tuplen(::Type{Tuple{T}}) where T = Val{1}()
tuplen(::Type{Tuple{T1, T2}}) where {T1, T2} = Val{2}()
tuplen(::Type{Tuple{T1, T2, T3}}) where {T1, T2, T3} = Val{3}()
...

In case this is an XY problem - what I’m actually trying to do is convert an array-of-tuples representation to a tuple-of-arrays representation, and with the above definition I can do:

"""
Convert an Array-of-Tuples to a Tuple-of-Arrays.
"""
toa(aot) = ntuple(i->getindex.(aot, i), tuplen(eltype(aot)))

without the above I could use length(first(aot)) to get the length but that assumes the array is not empty.

edit: as a nested XY problem, I just realized that Plots can take vectors of tuples natively and I don’t need to transform my data to tuple-of-vectors after all. Carry on…

2 Likes

Yup–it’s actually a simple one-liner:

julia> tuple_len(::NTuple{N, Any}) where {N} = Val{N}()
tuple_len (generic function with 1 method)

julia> tuple_len((1, 2, 3))
Val{3}()

julia> tuple_len((1, "hello", π))
Val{3}()

julia> tuple_len((1, "hello", π, [1]))
Val{4}()

This works because tuples (unlike all other types) are variadic, so you can dispatch on NTuple{Any, N} to match all tuples of length N.

11 Likes

Ah, thanks for that. I’d forgotten that tuple types are special.

2 Likes

So why isn’t it length()? Already defined in Base?

Sorry, that isn’t the same thing as I just realized. length() already exists in base for tuples,
but returns an integer for the length.

Also it’s not defined on the Tuple type, only tuple values:

julia> length(Tuple{Float64, Int64})
ERROR: MethodError: no method matching length(::Type{Tuple{Float64,Int64}})
Closest candidates are:
  length(::Core.SimpleVector) at essentials.jl:593

Right.

It’s also worth noting that the compiler is pretty good at dealing with tuple lengths as regular old integers using the built-in length function. For example, we can write a function that looks like it might be type-unstable:

julia> function maybe_type_unstable(x::Tuple)
         if length(x) == 2
           return 1.0
         else
           return "hello world"
         end
       end
maybe_type_unstable (generic function with 1 method)

but the compiler is smart enough to figure out the value of length(x) at compile time and infer the result:

julia> @code_warntype maybe_type_unstable((1, 2))
Variables
  #self#::Core.Compiler.Const(maybe_type_unstable, false)
  x::Tuple{Int64,Int64}

Body::Float64
...
julia> @code_warntype maybe_type_unstable((1, 2, 3))
Variables
  #self#::Core.Compiler.Const(maybe_type_unstable, false)
  x::Tuple{Int64,Int64,Int64}

Body::String
...
6 Likes

Sorry to came so late after the fight, but we finally don’t have a solution to get the length of a tuple type ?

Do you mean besides defining the one-liner function marked as a solution?

Yep this function gets the length of a tuple, not from the tuple type ad was requested at the beginning (and as it seems the OP is referenced) . Actually I found a solution asking another question there How can i index a tuple type? so nvm

Type{Tuple{…}} Is not the same as NTuple{…}

The solution in the other thread may be efficient, but is probably an implementation detail that cannot be relied upon.

As it is said in the accepted answer:

So you can use NTuple{N, T} to match any Tuple{...}. The only matter is that you want the function to take tuple types instead of tuples. The solution below is correct and do not rely in any implementation details, while it may be ugly and inefficient.

julia> tuple_type_length(x) = (n = -1; while !(x <: NTuple{n+=1, Any}); end; return n)

tuple_type_length (generic function with 1 method)

julia> tuple_type_length(Tuple{Int, Char})
2

julia> tuple_type_length(Tuple{Int, Char, String})
3

1 Like

One can avoid internals by using

julia> fieldcount(Tuple{Int,Float64})
2
5 Likes

Seems a good solution, I did not know fieldcount. My only worry is the caveat in the documentation:

Get the number of fields that an instance of the given type would have. An error is thrown if the type is too abstract to determine this.

Kinda hard to know beforehand what is a “too abstract” type.

Any concrete type should be fine with fieldtype. Even many incomplete types with defined-but-type-unspecified fields such as Complex{T} where T, Tuple{T,T} where T, and Tuple{Tuple{Vararg}} will work.

Examples of types that will fail are Tuple (with no further elaboration), Tuple{Vararg}, and NTuple{N,Any} where N. The number of fields that these types have is undefined.

So fieldcount should work with almost any type that actually has a number of fields. One might be able to cook up some pathological corner cases, but I haven’t thought of one yet. If you find one that fails that you think shouldn’t, you can consider opening a bug report.

2 Likes

Is there anything wrong with this variant of the accepted solution (sligthly adapted to work on Tuple types instead of Tuple instances)?

julia> tuple_len(::Type{<:NTuple{N, Any}}) where {N} = Val{N}()
tuple_len (generic function with 1 method)

julia> x = (1, "hello", 42.0)
(1, "hello", 42.0)

julia> tuple_len(typeof(x))
Val{3}()
2 Likes

I believe there is no problem, but I would adopt the simpler fieldcount solution now that I am aware of it, unless you want the code to fail in non-tuple types.

OK, but fieldcount gives a plain integer result, as opposed to the initial requirement that the result be a Val-wrapped value.

I agree that, since constant propagation is likely to work well in many cases, fieldcount would probably a very good and standard way to do this (but in those cases, the question then becomes: why not simply length?)

Is there any api guarantee that an NTuple will always have exactly N fields? Using fieldcount seems a bit ‘hacky’.

1 Like

length works if you have an instance of the object, true, but I thought the idea was to have a function that would work directly over the type?

Also, yes, fieldtype does not return a Val but neither does N in the parameter type annotation, both give Int values which can after be wrapped inside Val. It may be that one solution is recognized as type-stable by the compiler (i.e., recognizes the static mapping between input and output types) and the other solution isn’t, but this is speculation and should be checked.

1 Like