Idiomatic way to make lib functions auto-deref / auto-borrow input parameters

Suppose you have these code in your lib.rs:

struct MyType(String);

trait MyTrait {
    fn shared_behavior(&self);
}

impl MyTrait for MyType {
    fn shared_behavior(&self) {
        println!("{}", self.0)
    }
}

#[test]
fn test() {
    let my_var = MyType(String::from("Hello"));
    let my_ref = &my_var;
    my_ref.shared_behavior();
    my_var.shared_behavior();
    let my_rc = Rc::new(MyType(String::from("World")));
    my_rc.shared_behavior();
}

All of the above worked.

But if I create the following function:

fn do_it_twice(value: impl MyTrait) {
    value.shared_behavior();
    value.shared_behavior();
}

and then change the test function to:

#[test]
fn test() {
    let my_var = MyType(String::from("Hello"));
    let my_ref = &my_var;
    // do_it_twice(my_ref); // the trait bound `&MyType: MyTrait` is not satisfied [E0277]
    do_it_twice(my_var);
    let my_rc = Rc::new(MyType(String::from("World")));
    // do_it_twice(my_rc); // the trait bound `std::rc::Rc<MyType>: MyTrait` is not satisfied [E0277]
}

then my_ref (&MyType) and my_rc (Rc<MyType>) stopped working.

Mod #1 (reference)

fn do_it_twice(value: &impl MyTrait) {
    value.shared_behavior();
    value.shared_behavior();
}

#[test]
fn test() {
    let my_var = MyType(String::from("Hello"));
    let my_ref = &my_var;
    do_it_twice(my_ref);
    // do_it_twice(my_var); // mismatched types [E0308] expected `&impl MyTrait+Sized`, found `MyType` 
    let my_rc = Rc::new(MyType(String::from("World")));
    // do_it_twice(my_rc); // mismatched types [E0308] expected `&impl MyTrait+Sized`, found `Rc<MyType>` 
}

Mod #2 (Borrow)

fn do_it_twice<T: MyTrait>(value: impl Borrow<T>) {
    value.borrow().shared_behavior();
    value.borrow().shared_behavior();
}

#[test]
fn test() {
    let my_var = MyType(String::from("Hello"));
    let my_ref = &my_var;
    // do_it_twice(my_ref);
    // ^ type annotations needed [E0282] cannot infer type for type parameter `T` declared on the function `do_it_twice`
    do_it_twice::<MyType>(my_ref);
    do_it_twice(my_var);
    let my_rc = Rc::new(MyType(String::from("World")));
    // do_it_twice(my_rc);
    // ^ type annotations needed [E0282] cannot infer type for type parameter `T` declared on the function `do_it_twice`
    do_it_twice::<MyType>(my_rc);
}

Mod #3 (AsRef)

impl AsRef<MyType> for MyType {
    #[inline]
    fn as_ref(&self) -> &MyType {
        self
    }
}

fn do_it_twice<T: MyTrait>(value: impl AsRef<T>) {
    value.as_ref().shared_behavior();
    value.as_ref().shared_behavior();
}

#[test]
fn test() {
    let my_var = MyType(String::from("Hello"));
    let my_ref = &my_var;
    do_it_twice(my_ref);
    do_it_twice(my_var);
    let my_rc = Rc::new(MyType(String::from("World")));
    do_it_twice(my_rc);
}

Mod #4 (Deref)

fn do_it_twice(value: impl Deref<Target=impl MyTrait>) {
    value.shared_behavior();
    value.shared_behavior();
}

#[test]
fn test() {
    let my_var = MyType(String::from("Hello"));
    let my_ref = &my_var;
    do_it_twice(my_ref);
    // do_it_twice(my_var);
    // ^ the trait bound `MyType: std::ops::Deref` is not satisfied [E0277]
    // the trait `std::ops::Deref` is not implemented for `MyType`
    // Note: required by a bound in `do_it_twice`
    // Help: consider borrowing here
    let my_rc = Rc::new(MyType(String::from("World")));
    do_it_twice(my_rc);
}

Mod #5 (Blanket impl for Deref)

impl<M: MyTrait, T: Deref<Target = M>> MyTrait for T
{
    #[inline]
    fn shared_behavior(&self) {
        (**self).shared_behavior()
    }
}

fn do_it_twice(value: impl MyTrait) {
    value.shared_behavior();
    value.shared_behavior();
}

#[test]
fn test() {
    let my_var = MyType(String::from("Hello"));
    let my_ref = &my_var;
    do_it_twice(my_ref);
    do_it_twice(my_var);
    // (if the following line is uncommented): cannot move out of `my_var` because it is borrowed [E0505]
    // do_it_twice(my_ref);
    let my_rc = Rc::new(MyType(String::from("World")));
    do_it_twice(my_rc);
}

Comparison

Mod #1 is the simpliest way and, although it required a few more & marks to pass in MyType values (compared to the original version), it wouldn't consume the parameter as MyTrait::shared_behavior() only needed a reference to self.

All the alternative solutions consumed the various other input types (technically the reference is also consumed, but it's copied before that), and this side effect may or may not be intended.

Of course, we can add some restrictions, like for mod #5 we can do:

fn do_it_twice(value: impl MyTrait + Copy) {
    value.shared_behavior();
    value.shared_behavior();
}

so that any type that doesn't impl Copy wouldn't get consumed accidentally, but wait! Our blanket impl is for T: Deref, and the only type that implements both Copy and Deref in the standard library is the shared reference type &T.

So we're basically doing:

impl<T: MyTrait> MyTrait for &T {
    #[inline]
    fn shared_behavior(&self) {
        (**self).shared_behavior()
    }
}

and

fn do_it_twice(value: impl MyTrait + Copy) {
    value.shared_behavior();
    value.shared_behavior();
}

is now equivalent to:

fn do_it_twice(value: &impl MyTrait) {
    value.shared_behavior();
    value.shared_behavior();
}

for any type that impl MyTrait but does not impl Copy.

Conclusion

Covering all the babysitting is tempting, but it may cause more confusion as the side effects are often not expected.

Unless we expect the concrete type to impl Copy, it'd be better to just stick with passing &impl MyTraits.

Even if the type does impl Copy, the best we can do is merely reducing a tiny & mark at the call site.

What's your opinion on this topic? Do you have better solutions?

If you want to convert something to a reference internally for convenience, then accept an AsRef, basically your solution #3. If you are fine with a stronger relationship between your type and any generic types, and you want the blanket impl, then use Borrow instead of AsRef.

AsRef is commonly used for this (especially Path).

However, if you don't actually need to own the value, and your code can work fine with just a loan, I suggest don't take owned values. This will make the API clear about its requirements. Adding & at the call site is a normal thing to do in Rust.

Arguments generic over ownership have gotchas:

  • Deref won't kick in, so you'll get things like &Vec rather than &[] (that's a double indirection which can be slower, and it adds extra unnecessary version of your function in the binary).

  • Users can accidentally give away their value: foo(val); bar(val) won't compile, when foo(&val); bar(&val) would.

  • Type inference will be weaker. foo(&val.into()) can work with concrete arguments, but usually not with generic ones.

4 Likes

Yeah I figured that out after all these trial & errors. Looks like there's no auto-referencing/auto-dereferening sugar equivalent to (&val).method() => val.method() for fn parameters.

After digging deeper, the implicitness that comes along with the sugar makes more trouble to those who care about the details than the trouble it saves for careless beginners.

For self, most methods require a reference except those used for conversions which are typically named into_xxx.

Allowing auto borrowing would make ordinary function parameters behave the same way, but this time without such naming conventions, so people would have to check fn signatures to know whether the code is passing in a value or a reference at the call site.

I'm not sure what you mean by that, because method call syntax does auto-reference and auto-dereference as needed. Eg. if you have a method that takes &self, then you can call it on either a value of type Self or &Self. The former will auto-reference.

I meant there's no auto-reference and auto-dereference for parameters other than self and this sugar only applies to the method call syntax, for example:

    struct MyType(String);

    impl MyType {
        fn do_sth(&self) {}
    }
    let my_var = MyType(String::from("Hello"));
    
    my_var.do_sth();
    MyType::do_sth(&my_var);
    // MyType::do_sth(my_var); // mismatched types [E0308] expected `&MyType`, found `MyType` 

I've edited the original post.