Design/Best Practice for code generic over my simple trait?

I have a simple trait which simply declares 2 getter methods. Let's call it MyTrait. Each method borrows self immutably and returns some number.

The trait is exported and publicly/externally usable for any users of my lib. Within my lib, I am also a user/implementor of the trait (I have types which implement the trait). So let's say I also have defined in my lib a concrete type called MyElement for which I would like to implement MyTrait.

Now I can just implement it directly (impl MyTrait for MyElement), but there's a small problem that I ran into. Basically, I would like to be able to pass any kind of reference (whether it's a value of type &MyElement, Box<MyElement>, Arc<MyElement>, etc.), in addition to being able to pass an owned MyElement, to any of my lib's functions or types that work with MyTrait values generically. Also, by accepting either an owned value or ref, I can leave that decision up to the caller for maximum flexibility. How should I go about this?

I have come up with 3 ideas:

  1. As a user of the trait, simply implement it for all of these concrete types (i.e. MyElement, &MyElement, Box<MyElement>, etc.). This is the most straightforward but feels overly-explicit, boilerplate-heavy, and not very user-friendly. It requires multiple impls for the same underlying type (i.e. MyElement) and doesn't cover custom pointer types up front.

  2. Define a MyTrait blanket impl within the lib that defines MyTrait. For example:

impl<T, U> MyTrait for T
where
    T: Deref<Target=U>,
    U: MyTrait,

Now all I'd have to do is implement MyTrait for MyElement.

  1. Just be more broad in what my generic library functions and types accept for those that accept some T where T: MyTrait. E.g. use bounds like this instead:
// ...
where
    T: Borrow<U>,
    U: MyTrait,
// ...

The con/issue with this is, now I have signatures which are more complex and less ergonomic to work with (and now every time I want to call a trait method of MyTrait I need to call .borrow() on it first). Plus, these more complex bounds/signatures will propagate to all of the other places where I want to work with some type implementing MyTrait as well. Therefore, I have essentially decided against going this route.


I'm leaning towards just doing #2, but maybe that's a bad idea? idk what to do. idk know what's best here.

Hopefully that all makes sense. Thanks

I wouldn't design to take by value if the method has no need for it. If you do, it will be a pain to use method call syntax when you don't want to give up ownership.

And method resolution will take care of the other cases you listed due to autoderef.

Standard practice when providing a trait is to provide generic impls of this sort:

impl<T: MyTrait> MyTrait for &T {...}
impl<T: MyTrait> MyTrait for Box<T> {...}
impl<T: MyTrait> MyTrait for Rc<T> {...}
impl<T: MyTrait> MyTrait for Arc<T> {...}

This doesn't generalize over different smart pointers, but it avoids the restrictions on further implementation created by having a blanket impl. Think of it as a sub-case of the general practice of giving your trait appropriate implementations for all standard-library types.

(I agree with quinedot that you should just take <T: MyTrait> &T in plain functions. These impls are valuable in situations where that's not a sufficiently general option because the T is stored for longer than a single function call.)

2 Likes

The trait's methods all only borrow self immutably (&self). I checked out the playground, but I have functions and types which need to accept values whose type is generic (e.g. T where T: MyTrait), not whose type is concrete as in MyType.

Awesome thanks! I think this is what I was looking for. I wasn't aware of that standard practice.

And it does appear that having that blanket implementation from #2 would prevent users from providing a custom impl of the trait for any type which dereferences to some type that implements the trait. Interestingly this also applies to any Deref type no matter what it's Deref target is, which I'm not sure I can wrap my head around atm (Rust Playground).

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.