Generic over trait and Borrow type, PhantomData

I'm attempting to make some types that that were originally defined as a File-only wrappers, be generic over the actual extension trait they use (PosRead) and also generic over different Borrow types. I have it compiling, tests and clippy passing, but I'm not sure if its the best way to do it, either for my code hygiene or ergonomics.

To explain the Borrow aspect better, I have a type ReadSlice that I want to be able to construct with an owned value like ReadSlice<File, File> or to use a shared reference: ReadSlice<File, &File> or ReadSlice<File, Arc<File>>, where in these cases File implements the PosRead trait.

Here is my current definition:

#[derive(Debug)]
pub struct ReadSlice<P, B>
where P: PosRead, B: Borrow<P>
{
    start: u64,
    pos: u64,
    end: u64,
    file: B,
    phantom: PhantomData<fn() -> P>
}

Firstly, there were various problems with anything short of both generic parameters. When introducing B: Borrow<P> however, the compiler complains of an unused P unless the PhantomData is also included. That was somewhat surprising to me because the B parameter is used and defined in terms of P. Why isn't that sufficient?

Next, PhantomData rustdoc seems to suggest I use PhantomData<*const P> "so as not to indicate ownership". I'm not hiding a raw pointer here, an the file field and Borrow type should already deal with the ownership/lifetime aspects, correct? However with *const P I loose Sync/Send for the type, which is undesirable.

I then found this fiendish table in the Nomicon, which lead me to the PhantomData<fn() -> P> form not mentioned in the rustdoc. I'm not entirely sure if I want to preserve variance over P, and I didn't find a case where I ran afoul of Drop Check when just using PhantomData<P>. Is there something better I should be using here?

Finally, when constructed this type the compiler can't infer P from the B type as passed? I end up needing to construct like this, for all but the owned File type:

let rslice = ReadSlice::<File, _>::new(Arc::new(file), 0, length);

I tried to use a default generic parameter, e.g:

pub struct ReadSlice<P, B=Borrow<P>>
where P: PosRead, B: Borrow<P>

…which caused another problem instead of helping. In retrospect, this isn't likely the way to get the compiler to infer the P type from B. Is there another? Its not terrible now, but is there some way to make this more ergonomic?

The full work-in-progress is at: dekellum/olio#1.

Original release was previously posted as New crate: olio.

The issue is that Borrow<P> doesn’t unambiguously select what P is because Borrow can be implemented multiple times by a certain type, and each impl can return a different P: PosRead.

If, however, you were to use a trait bound with an associated type, say B: Deref<Target = P>, then you don’t need the phantom data because the Deref impl unambiguously determines P since Deref cannot be implemented multiple times with a different associated type.

I think PhantomData<fn() -> P> is likely the more conservative approach, although PhantomData<P> will probably work just as well given the real types you’ll be using. But, I’ve not thought too hard about this particular aspect.

If I change to B: Deref<Target = P> it looks similar to when I had initially tried with B: AsRef<P>—an owned value File does not implement Deref<File> only &File, Arc<File>, etc. After trying AsRef, I guess I latched onto Borrow for this reason. Or am I not fully understanding your suggesting; is there some way to still use Deref to make it generic over P, &P, Arc<P> where P is PosRead? I'm assuming there is some good reason why Deref doesn't include an implementation for an owned value.

Sorry, I wasn’t clear - I used Deref only for illustration purposes; it won’t work for owned values, as you’ve already noted. AsRef wouldn’t have helped for the same reason as Borrow - there’s no associated type to latch on to.

As for a way out, maybe impl PosRead directly for the different types you want (eg File, &File, and Arc<File>). Perhaps you can actually blanket impl it for R: Read. Then Borrow isn’t needed at all.

1 Like

Thanks very much for these suggestions @vitalyd. :smile: Subdividing the problem by implementing PosRead on the all the desired reference types allows for improvement:

  • I was able to implement PosRead generically and once over all Borrow<File> types.

  • Then as you suggest, the ReadPos and ReadSlice wrapper types need only be generic over PosRead so I can drop the spooky PhantomData and there is no longer any type inference challenges nor need for turbofish.

  • I can't measure any performance degradation.

All updates in:

https://github.com/dekellum/olio/pull/1

1 Like