Using From and Into with &T

Hi,

I am writing a piece of code that is suppose to check if a given object (could be also a mega complex thing) is placed at center of the scree, something like:

fn is_placed_at_origin(object: &Whatever) -> bool

So I was thinking to define a Placeable trait that provides a getter that returns the position and all 2D objects could implement it, when I thought about using From and Into, but then I realized that maybe they don't work pretty well with references. Check my implementation:

#[derive(Copy, Clone)]
struct Vec2 {
    x: i32,
    y: i32,
}

// Not Copy, since it is a complex struct
struct ComplexStuff {
    position: Vec2,
    //...
}

// Solution 1
// From conversion: maybe cleaner, but without a reference it will consume the provided object
impl From<&ComplexStuff> for Vec2 {
    fn from(other: &ComplexStuff) -> Vec2 {
        other.position
    }
}

// Solution 2
// Deref _trick_
impl std::ops::Deref for ComplexStuff {
    type Target = Vec2;
    
    fn deref(&self) -> &Self::Target {
        &self.position
    }
}

fn is_at_origin_from<T: Into<Vec2>>(p: &T) -> bool {
    let q: Vec2 = p.into();
    q.x == 0 && q.y == 0
}

fn is_at_origin_deref(p: &Vec2) -> bool {
    p.x == 0 && p.y == 0
}

fn main(){
    let c0 = ComplexStuff{position: Vec2{x: 0, y: 0}};
    assert!(is_at_origin_deref(&c0));
    //assert!(is_at_origin_from(&c0));
}

Here it is the error reported by the compiler:

   |
32 |     let q: Vec2 = p.into();
   |                     ^^^^ the trait `From<&T>` is not implemented for `Vec2`
   |
   = note: required because of the requirements on the impl of `Into<Vec2>` for `&T`

I think the problem here is that I am forcing something that is not allowed, that is I am implementing From<&T> instead of From<T> and so the autogenerated symmetric Into is broken, but I am just smelling stuff, but not sure to have got it which is the real problem :smiling_face:

Furthermore do you thing that Deref solution is a good alternative or maybe I should define a Placeable trait as I was thinking from the beginning?

Thank you

You shouldn't use From or Deref for this. Just make your own trait.

1 Like

You might consider using AsRef when doing (cheap) reference-to-reference conversion.

#[derive(Copy, Clone)]
struct Vec2 {
    x: i32,
    y: i32,
}

// Not Copy, since it is a complex struct
struct ComplexStuff {
    position: Vec2,
    //...
}

impl AsRef<Vec2> for ComplexStuff {
    fn as_ref(&self) -> &Vec2 {
        &self.position
    }
}

fn is_at_origin<T: AsRef<Vec2>>(p: T) -> bool {
    let q: &Vec2 = p.as_ref();
    q.x == 0 && q.y == 0
}

fn main(){
    let c0 = ComplexStuff{position: Vec2{x: 0, y: 0}};
    assert!(is_at_origin(&c0));
}

(Playground)

You require that T implements Into, but p has the type &T which may not. Redefining the argument type will let this compile:

fn is_at_origin_from<T: Into<Vec2>>(p: T) -> bool {
    let q: Vec2 = p.into();
    q.x == 0 && q.y == 0
}

//…

fn main(){
    let c0 = ComplexStuff{position: Vec2{x: 0, y: 0}};
    assert!(is_at_origin_deref(&c0));
    assert!(is_at_origin_from(&c0));
}
1 Like

Yeah, I am tempted doing that, but I remember that somewhere (cannot remember if it was on this forum or on Blandy's book) I read that it is a nice thing to keep your functions interface more flexible using From or AsRef or Deref when it is possible, so I was wondering if this is the case.

Sure, you should use those traits when they are appropriate for your use-case, but they are not appropriate for your use-case.

  • The From trait is used to convert between different ways of storing the same data. It shouldn't be used to access one field of a larger struct.
  • The Deref trait is used for smart pointers. Your struct is not a smart pointer.
  • The AsRef trait is used in a similar way to From except that it is always a reference-to-reference conversion, so the conversion must be free and it must not destroy the original value.

None of these descriptions match what you're doing.

4 Likes

Regarding From, I would concur. Not sure about AsRef. I think it depends on semantics. If &T can be "seen" as &U, then implementing AsRef<U> for T can make sense, even if that means one field of a larger struct is returned.

However, it's not clear whether ComplexStuff really should be convertible into a Vec2 without further semantics. Following @alice's advice, you could do:

trait HasPosition {
    fn position(&self) -> Vec2;
}

impl HasPosition for ComplexStuff {
    fn position(&self) -> Vec2 {
        self.position
    }
}

fn is_at_origin<T: HasPosition>(p: &T) -> bool {
    let pos = p.position();
    pos.x == 0 && pos.y == 0
}

(Playground)

I totally agree here: don't use Deref. The documentation explicitly warns about it:

Implementing Deref for smart pointers makes accessing the data behind them convenient, which is why they implement Deref. On the other hand, the rules regarding Deref and DerefMut were designed specifically to accommodate smart pointers. Because of this, Deref should only be implemented for smart pointers to avoid confusion.

While I think this warning isn't optimal (#91004), using Deref should only be used when deref-coercion is good to have (which is most often not the case unless you have a smart-pointer).


Now that I think about it, maybe there could even be cases where providing an implementation of From which just extracts a single field of a struct and discards the rest could be helpful. But I generally see the problem with From/Into and AsRef that these traits lack semantics other than "it's a conversion".

It's arguable whether ComplexStuff should be convertible into Vec2. In std we have all sort of weird conversions. For example, we can convert a String into Vec<u8> (without specifying the character encoding!). On the other hand, there exists no fallible conversion from String to i32. :man_shrugging:

I sometimes doubt whether From/Into and AsRef are a good thing to have at all because they don't specify the nature of the conversion they do.

1 Like

I'd be wary of this advice. The compile-time cost of making all your functions generic like this is substantial, and the convenience win is often minimal. For example, if your function takes a u64, just take a u64. Taking an impl Into<u64> just isn't worth it -- especially since it means that you can no longer pass a literal to it. It's just not that hard for people to put a .into() on the call if they want.

4 Likes