Change parameter type to its trait means I don't pass as a reference


#1

So I have a function defined like so -

 fn read_u64(index: &MmapMut, pos: usize) -> Result<u64> {

Which I would call like so -

let tkey = read_u64(&index, pos as usize)?;

But then I decided I didn’t want my read_u64 function to only work on MmapMut, I would rather use it’s trait like so -

fn read_u64(index: &Deref<Target=[u8]>, pos: usize) -> Result<u64> {

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.


#2

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.


#3

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
https://docs.rs/memmap/0.6.2/memmap/struct.MmapMut.html

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…

Thanks.


#4

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.


#5

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, 

No such impl exists for Deref, AFAIK.


#6

I’ve done a bit more reading up on traits from the link you posted. I think the vital point that made it click more was this line

A trait object points to an instance of a type that implements the trait we specify.

So I see now that a trait object is already a reference…

A bit more reading up on Deref and Asref also makes things a lot clearer. Turns out what I actually want to do with my function is

fn read_u64(index: &[u8], pos: usize) -> Result<u64> {

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!


#7

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.