This doesn't compile. I have to remove the reference(&) from the calling function to make the compiler happy -
let tkey = read_u64(index, pos as usize)?;
I don't quite understand what is going on here. Why does changing the parameter type mean I no longer need to pass it as a reference? Is the function read_u64 still just borrowing the index parameter, I would presume so?
I think there is something fundamental here that I'm not getting. Any pointers would be appreciated.. Thanks.
You changed the fn to take a trait object, and specifically one that’s a reference. The Rust book has some material on trait objects so I won’t bore you here (unless you want that, in which case let me know).
The trait object here specifically is some implementation of Deref<Target=[u8]>. So you can pass in a reference to any type that implements this trait. A &MmapMut, which it sounds like you ended up with, does not implement that trait - only MMapMut does (note that a T and &T are considered different types).
I suspect you don’t want a trait object here - you want a generic signature:
fn read_u64<M: AsRef<[u8]>>(index: M, ...
AsRef is more flexible (for callers) than Deref, and generally conveys the intent better.
I’m on mobile so will stop here - feel free to ask for clarification.
Ok, I think I understand a little bit about why I am getting confused.
Looking at the docs for MmapMut it implies that it implements the Deref trait
But it seems looking further, this is actually a core trait, built into the language and not just some standard crate. I will need to read further about Deref and Asref etc..
I'm not sure I fully understand what you mean by T and &T being different types. When I change the parameter to being &Deref<> I am asking for a reference to an object that implements the Deref trait and not just the object? Is this because Deref is a special trait?
I think I will need to experiment with some code in the morning..
By different types I mean the type system considers them as different - the borrow isn’t a "modifier" of sorts. The important bit here is one can implement traits for owned and borrowed values, and if you implement a trait only for a borrow (or only for an owned value), it doesn’t mean the other "inherits" the impl.
There’s nothing special about Deref except it supports deref coercions, which is a lang feature. But that feature isn’t relevant to this particular case.
Here is an example using AsRef - you can see that the caller can pick between sending a reference, and retaining ownership, or it can send the value and moving ownership into the fn. The function doesn’t care and thus provides the caller with flexibility to decide.
By the way, your next question might be: if trait impls aren’t "inherited" between values and references, then why does the example I put in the playground work with &MmapMut if MmapMut only implements AsRef<[u8]> for MmapMut and not &MmapMut?
That’s because stdlib has the following blanket impl:
impl<'a, T, U> AsRef<U> for &'a T
where
T: AsRef<U> + ?Sized,
U: ?Sized,
and because MmapMut implements the deref trait I can just call it like -
let tkey = read_u64(&index, pos as usize)?;
And the deref will coerce the MmapMut to a [u8] for me. I don't need to mention Deref at any point.. Or I can just call it with a standard [u8] array if I want.
Thanks for your help. It's been an interesting journey, and I've discovered a great feature of Rust in the process!
Not necessarily - you can have owned trait objects as well (e.g. Box<SomeTrait>, Rc<SomeTrait>, Arc<SomeTrait>). But a &SomeTrait trait object is indeed a reference.
Yes, possibly - it really depends on how much flexibility and ergonomics you want to give to the caller of this function, specifically around ownership. Taking a &[u8] works nicely for types that impl Deref<Target=[u8]>, and that may be enough for your needs. But if a type implements AsRef<[u8]> but not Deref<Target=[u8]>, the caller will need to manually call as_ref() on their type to give you the slice. Whereas if you take T: AsRef<[u8]>, they can just pass a reference to the type itself - this is pretty much the example I gave above.
But if you're working with logical wrappers over byte buffers, then Deref<Target=[u8]> will likely cover all your cases, deref coercion will make calls ergonomic enough, and there's no need to over[complicate|engineer] things.