It might scale just fine, because LLVM might discover the pattern of your
"giant switch statement" and essentially compute a jump location. Even if not,
if you're calling this repeatedly with a tuple of consistent length, the
branches become predictable and then they kind of don't count. So in most
cases I doubt it's possible to beat the performance of the solution you
posted.
For small tuples, you can essentially match the performance of your generated
function using "lispy" recursion. This is more friendly for the compiler,
which you may or may not care about. (It's something we care about in Base,
but there's no reason you necessarily have to worry about it in your own
code.)
First, a couple of convenience functions:
@inline push(t::Tuple, item) = (t..., item)
@inline unshift(t::Tuple, item) = (item, t...)
# we'll also need something like shift!, but that's already present
# and called Base.tail
Now let's implement your function:
drop_ith(t, i) = 1 <= i <= length(t) ?
_drop_ith((), t, i) :
throw(ArgumentError("need 1 <= $i <= $(length(t))"))
@inline function _drop_ith(out, t, i)
h, t1 = t[1], Base.tail(t)
_drop_ith(length(out)+1 == i ? unshift(out, h) : push(out, h), t1, i)
end
_drop_ith(out, ::Tuple{}, i) = Base.tail(out)
This uses the sneaky trick of preserving type-stability by putting the element
you want to drop into the first position, thus ensuring that the tuple always
grows by 1 on each level of recursion. (For tuples, the length is part of the
type, so making the length predictable is critical for type-stability.) At the
end, you strip off the first element.
This works well up to tuples of size 14; any bigger, and a variable called
MAX_TUPLETYPE_LEN ends up kicking in and hurting performance. However, it has
the advantage of not needing Val tricks or @generated functions. In cases
where there's less run-time checking, this approach can usually match
@generated solutions, so the general technique is worth knowing.
Best,
--Tim