Semi-homogeneous collection - all working now except desired build method

I'm running into an interesting unconstrained type parameter issue with the following:

impl<'a, T: 'a, S1, S2> ::std::ops::Index<u8> for (S1, S2)
where
    Self: 'a,
    S1: AsRef<T> + 'a,
    S2: AsRef<T> + 'a,
{
    type Output = T;
    fn index(&self, idx: u8) -> Option<Self::Output> {
        match idx {
            1 => Some(AsRef::<T>::as_ref(&self.1)),
            0 => Some(AsRef::<T>::as_ref(&self.0)),
            _ => None,
        }
    }
}

I get that technically tuple structs of the form (S1, S2, ...) aren't on their own constrained by the type T, so there may not be a simple way around this. The standard answer for ordinary structs is to add phantom data, but I don't want to lose the simplicity of the tuple struct by going to fields just so I can label one a phantom field and then ignore it.

I could try affixing the phantom data to the end of the tuple as (S1, S2, ..., PhantomData<T>). Because phantom data is zero sized would the appended version have the same memory layout? If so I could transmute to "attach" the proper phantom data (depending on the T type I want to reference). Or if not, then would ((S1, S2), PhantomData<T>) preserve the layout? Or, as another option, could I implement From<(S1, S2)> for (S1, S2, PhantomData<T>) and then implement the indexing on objects that are Into<(S1, S2, PhantomData<T>)>? I'm pretty unclear on what implementing on a dyn Trait actually would actually accomplish...

The problem here is that if S1 and S2 both implement AsRef<T1> and AsRef<T2> for two types T1 and T2, then your code would generate multiple impls of the trait Index<u8> for (S1, S2), and this is not allowed.

Have you considered using a trait like Deref where you cannot have multiple allowed target types? Or perhaps not using a trait at all and requiring that S1 = S2?

I am VERY specifically making sure that T will be uniform. In my final use case it will be ensured that all S varieties implement AsRef<dyn CommonTrait> (as well as AsMut<dyn CommonTrait> for iterating).

I may have talked myself through the phantom data bit, but now it seems I've punted the problem into a new area (playground).

Or maybe not (playground v2)... Now I'm really baffled, as I do enforce Self: 'a.

I mean, if you pick S1 = S2 = String you already have a case where T has multiple choices.

1 Like

I was originally looking to implement it using a new IndexAs<T, usize> trait. I very much want to be able to have type inference work out the T, but I guess I'm okay falling back to a more explicit form.

Though check out the iteration implementation (playground) showing working type inference, including having an alternate AsRef<dyn Nothing> target that it successfully avoids.

Well now I definitely need help figuring out this mess! Because "tuples are always foreign", okay.... But what's that got to do with the price of onions?

Either the trait or the type must be your own when implementing a trait. The "tuples are always foreign" message says that tuples don't count as your own type, hence the trait must be defined in your crate.

That helps... So I think I'm on the right track with implementing From<(S1, S2, S3)> for a new struct with a base of the tuple and a phantom field showing the as ref target type and implementing on dyn Into<WithTarget<'a, T, S1, S2, S3>>. But now it's claiming usize is not defined??? playground

It's claiming that usize is not defined in the current crate, which is true.

This is a rare instance where compiler errors are just completely baffling. I tried type Idx = usize;, and that doesn't change anything so it correctly states again that usize is not defined in the current crate. I've also tried implementing Into instead of From because the other half of the error message says dyn Into<...> isn't defined, but that also leaves the error unchanged. So.... I'm guessing implementing on a dyn Trait is the problem? Or is it because the base trait isn't defined in the crate? Do I need to make my own Into/From wrapper? Perhaps ViewAs<WithTargetTuple3<...>>

Alright. I think I'm closing in on a workable path. I've got it down to an issue with Index wanting me to return an &Option<&T>, which is a great way to return an immediately dropped value. (playground)

And if I try to take the Option out and panic!() on an invalid index my AsRef<usize> no longer actually seems to give an &usize. (playground)

The method signature should be

fn index(&self, idx: usize) -> &Self::Output {
//                             ^ New
1 Like

Oh dang, so it should be. Oops!

Well then! I'm now at a disconnect between fully and apparently successfully implementing my ViewAs trait on a reference &(S1, S2, S3), and not actually being able to index on the object that I'm now pretty sure is successfully a dyn ViewAs<'a, T, WithTarget3<'a, T, S1, S2, S3>>. (more playground)

Is it possible the compiler is too quick to suggest going to tuple.0 style indexing? It definitely doesn't say the trait Index is not implemented... Or not [E608], so where am I still not connecting all the dots?

Well, I think one disconnect might be because the signature for Index elides the lifetime. I've simplified things quite a bit, but still can't get the anonymous lifetime in Index to infer itself as 'a. (playground)

I feel like I'm just wildly slapping 'a on everything and missing something obvious (playground)

I haven't taken the time to fully grok your goals, but maybe this gets you closer. (Probably more lifetime generics could be dropped or at least elided.)

1 Like

Ahh, kind of makes sense that using it via accessing the trait directly would help elide the correct lifetime in. Also, holy crap it works!!!!

If you want more details of the current windmill I'm tilting at, check out this thread. There were a couple of pretty nifty solutions that I'm borrowing heavily from.

Uniform tuple in Rust is [T; 2]. Tuples really aren't meant to be used as lists/arrays. Every week I see someone get stuck on that in Rust. The language will really make you have a very bad time if you go that path. There are no generic variadics. There's no sensible way to get length. In Rust tuples are like struct with predefined field names, not a fancy syntax for an array.

1 Like

Heh, I'm well aware of how odd what I'm trying to do is. Some people try making self-referential graph/tree structs, I'm apparently quite fixated on trying to tidy up wasted enum space. But I'm learning a lot in the process!

Here's the UCG topic on homogeneous layout (still open and non-normative even once closed, unless/until RFC'd).

The structs A/B/C are currently just wrappers for usize, as that's all that's needed to demonstrate that the concept works. What I'm actually working with is five (or more) different structs representing different solve orders (1-4), parameterized over a type (f64/f32, and perhaps even looking at the fast_floats crate), and further parameterized by varying degrees of freedom (3-6). And I still plan on looking into options for SIMD.

So, I end up with structs that can range from 60 bytes to at least 672 bytes (for now). But they all have the same functionality, interface, and operation. Thus I want to be able to create a fixed collection of them to operate over. I could hand-write structs and methods to deal with one/two/three/four/five/six/etc. instances, or I can collect them into something that can operate the way I want. So my options are:

  • Hand-made 'collections'
  • Vec<Box<dyn Trait>>
  • [HorridEnum; N]
  • Or my tuple insanity

I fully plan on testing and comparing the results at the end. If the end result is that Vec<Box<dyn Trait>> performs the same, then that's a win for me either way.