Is there an idiom for constructing a heterogeneous tuple?

So, is there a preferred way for for i = 1:5 ... end with N known to the compiler, that permits type-instability with respect to i?

That is, each iteration might have entirely different types, and might need entirely different optimizations. But it might also be possible to make it a loop, who knows.

For example, a natural way of implementing ntuple could be:

julia> function mk_ntup(f, ::Val{N}) where N
       res = ()
       for i=1:N
       res = (res..., f(i))
       return res

But that does not cut the cake: The return value for e.g. mk_ntup(identity, Val(5)) is inferred as Tuple{Vararg{Int64,N} where N}.

A macro like @repeat i = 1:N could do the job, but would be worse if there are no instabilities: Then, it is faster to compile and probably faster to execute as a loop, i.e. with a loop counter and branches back; anyways, it is llvm’s job to know whether to unroll a loop or not (trade off decoding time, code size, L1i vs cost of loop-counter, branch, advantages of using immediates).

This seems distinct enough from the original topic I’ve split it out. The standard way to do this is with ntuple:

julia> f(i) = i == 1 ? 1 : i == 2 ? 2.0 : i == 3 ? 0x03 : "more"
f (generic function with 1 method)

julia> ntuple(f, Val(3))
(1, 2.0, 0x03)

julia> @code_warntype ntuple(f, Val(3))
  #self#::Core.Compiler.Const(ntuple, false)
  f::Core.Compiler.Const(f, false)
  #unused#::Core.Compiler.Const(Val{3}(), false)


You don’t even need Val if it’s a hardcoded constant:

julia> g() = ntuple(f, 5)
g (generic function with 1 method)

julia> @code_warntype g()
  #self#::Core.Compiler.Const(g, false)