Fundamentally, '@ rest' should not return a tuple of the tail. It is nearly non-viable while maintaining a zero cost philosophy. However, inductive definitions remain possible. I've been playing around myself with working on a zero overhead general purpose generics library, and the technology there applies equally well to this more limited case.
The trick is this: Rather than an inductively defined tuple, we have an inductively defined 'progress system'.
Imagine a setup more like the following:
pub trait ValidTuplePart<Struct> {}
pub trait IsTuple : ValidTuplePart<Self::Struct> {
type Struct;
}
impl ValidTuplePart<TupleEnd> for () {}
impl<A: ?Sized> ValidTuplePart<TupleEnd> for (A,) {}
impl<A: ?Sized> ValidTuplePart<TupleCons<A, TupleEnd>> for (A,) {}
impl<A: ?Sized> IsTuple for (A,) {
type Struct = TupleCons<A, TupleEnd>;
}
impl<A, B: ?Sized> ValidTuplePart<TupleEnd> for (A, B) {}
impl<A, B: ?Sized> ValidTuplePart<TupleCons<B, TupleEnd>> for (A, B) {}
impl<A, B: ?Sized> ValidTuplePart<TupleCons<A, TupleCons<B, TupleEnd>>> for (A, B) {}
impl<A, B: ?Sized> IsTuple for (A, B) {
type Struct = TupleCons<A, TupleCons<B, TupleEnd>>;
}
impl<A, B, C: ?Sized> ValidTuplePart<TupleEnd> for (A, B, C) {}
impl<A, B, C: ?Sized> ValidTuplePart<TupleCons<C, TupleEnd>> for (A, B, C) {}
impl<A, B, C: ?Sized> ValidTuplePart<TupleCons<A, TupleCons<B, TupleCons<C, TupleEnd>>>> for (A, B, C) {}
impl<A, B, C: ?Sized> IsTuple for (A, B, C) {
type Struct = TupleCons<A, TupleCons<B, TupleCons<C, TupleEnd>>>;
}
pub const fn head_offset<
Head: ?Sized,
Tail,
Tuple: ?Sized + ValidTuplePart<TupleCons<Head, Tail>>>() -> isize;
Here, we can just pass around the original or pointers to it, and use head_offset
to find the currently relevant field, regardless of the layout order. Writing an ergonomic interface around this for all cases like Rc and such is tricky. But this is sufficient to make it work in a way that after monomorphization and only a tiny bit of inlining things like functions that are actually safe transmutes or offsets, combined the various recursions of these functions should be zero cost.
Slapping inline on those transmutes and offsets should be trivial (though making sure that it optimizes them the same as ordinary field access might require setting some flags maybe?) and teaching the compiler to inline calls when there is a decreasing tuple argument should therefore be sufficient on top of a tiny bit of magic.
It would also benefit from teaching the coherence checker that IsTuple
as a bound captures exactly and only the tuple types, so instances for tuple types can be declared in one fell swoop, rather than merely being able to define functions that operate over all tuples.
Explanation of how it is used:
You can recursively go through the fields of a tuple by starting at the IsTuple::Struct
type until you reach TupleEnd
. At each TupleCons
you can locate the current field you want to focus on. Safety guarantees include that if you start at IsTuple::Struct
and end at TupleEnd
you will have handled all the fields exactly once, fully initializing or deinitializing the entire thing.
Since the head_offset
function takes a specific tuple type, reordering tuple fields in memory is fine. It could even be changed into a function that takes an integer index, though that would add a potential complexity around tuples consisting of more than usize elements (obviously a lot of which would have to be ZSTs, and admittedly probably will make most compilers unhappy anyway), as simply iterating up to build the constant passed in would then be unsafe. As its current for is unary, it is a genuine natural number rather than a bounded value. Still, probably best to make sure that mixups like the Rc overflow are avoided upfront rather than simply assume that no such tuples will ever be used, or have (admittedly perhaps const time) panics on overflow.