Different impl of trait for T, &T, and &mut T

Hello, is there any way to implement a trait for borrowed versions of some type? So,

trait Foo {}
impl <T:bounds> Foo for T {}
impl <T:bounds> Foo for &T {}
impl <T:bounds> Foo for &mut T {}

Doing like this will result in a collision, since technically the first impl covers the other two. Is there any way to have different impls for owned and borrowed (ref and mut) with generics? Or away to get a similar effect? Much appreciated.

It might help to describe the actual problem you're trying to solve - sometimes its easier to suggest workarounds that way.

3 Likes

I want to be able to get data from some struct (which essentially holds vectors of trait objects), and determine the type of retrieval (clone, borrow, mut borrow) by the generic argument.

I mean you could do the following:

  • Wrap the T in some struct: DataWrapper(Data).
  • Then implement Deref and DerefMut on DataWrapper, this way you can detect shared and exclusive borrows.
    Although, I'm still not entirely clear what you want to achieve by "detecting" the type of borrow.

I want the user to be able to specify (via generics) what type of borrow they want, like so:

let var: &mut SomeComponent=storage.into() //mutable borrow
//or
let var: SomeComponent=storage.into() //clone 
//etc...

There is no problem implementing traits for &T and &mut T if T is a concrete type. What you're trying to do is a blanket impl, and those always cause issues. Strictly speaking, what you're trying to do in your post requires impl specialization, which is currently an unstable feature and will likely stay that way for a long time. However, it is super likely that you are trying to generalize beyond the bounds of reasonable. Rust is quite peckish about generic types, and generally strongly prefers working with non-generic code. Boilerplate can be abstracted via macros.

Your specific use case may have a better solution, or maybe it has some special requirements which make the macro-based approach unworkable. That's impossible to say without more specifics about your problem.

1 Like

I would definitely disagree that Rust

Generics are pretty much everywhere in Rust. Like everywhere! In fact, coming from C++(technically C++ allows you do pretty much anything with templates, but it's absolutely hell to work with, (this might be better with Concepts or whatever)/C#/Java (not Haskel!), Rust's type system is incredibly robust and powerful. You could argue that the language would prefer non-generic code simply because it is much easier to work with implementation wise, I suppose. Anyhow, I actually have specialization enabled (I needed it for something else), so...
Anyway, I figured out a workaround that will have to suffice, for now. Thanks to all involved!

While I have to agree that one should try generics first before resorting to macros, I don't specifically see the motivation for using generics and into() in your specific case. The way this pattern is usually implemented is to provide separate functions or trait impls for obtaining a reference, a mutable reference, and a value. E.g., AsRef or Borrow, AsMut or BorrowMut, and From or Into, respectively.

It is very possible that macros would be better suited for my specific situation, upon reflection. My knowledge is incomplete of macros at present, but I might give it a try. I just don't see @afetisov 's point that rust prefers working with non-generic code.

I'm just speaking from my experience. Rust works best when you use generics as strongly typed macros, using concrete types as much as possible. If you try to dive heavily into generics, you will encounter all kinds of issues and language limitation. Your compile times will blow up (generics are monorphized in the crates that instantiate them, in each crate individually). Your code size will blow up, worst case exponentially (for the same reasons: if you declare a wrapper type struct Foo(Vec<u8>) and impl AsRef<[u8]> on it, each time you pass it as an impl AsRef argument into a function, you will get an entirely new monomorphization, even though it is trivially the same as for Vec<u8>). You will encounter issues with huge trait bounds repeated on every function. You will encounter weird type inference errors (remember that Rust trait system is Turing complete, and it's very easy to make complex generic bounds). You will hit orphan rules and overlapping impls. You will hit missing features, like lacking const generics, GATs and associated traits. You will hit bugs in the trait resolution engine (and yes, they exist, just not in simple code). Etc, etc.

Everything is so much easier when you can keep your types and functions concrete, and you should strive to do it as much as reasonably possible. Think thrice if you want to turn a simple &[u8] parameter into AsRef<[u8]>, you may be paying dearly for a slight convenience.

For a language which is really designed around generics, you can look at Scala or OCaml. Hell, even Java works smoother with generic code (within the rather stringent confines of it type system).

Don't get me wrong, I love writing generic code myself. That's why I have hit my head on all those issues. Like with macros, you should always weigh the benefits of a solution against its costs, and ask yourself if generics (or macros) are really pulling their weight.

3 Likes

No, that's not what I'm saying. I don't think you need any macros for this, either. Just implement the 3 different traits (or maybe even inherent associated functions) and you will get idiomatically-named conversion functions.

Uh-oh, those are some pretty strong and unfounded accusations.

Well, Rust uses the same ideas for its trait system. It's constraints on types all the way down.

Excuse me? That is just blatantly false. Java's variance checking is unsound, and the very Scala you cited as being better at generics inherits the same issue from it. It is not an achievement for a language to be more lenient at the cost of being incorrect. That is not what ease of use means at all.

While there are still some missing features around generics in Rust, there is really only one thing that can be a deal breaker, and that's the full const generic story. Variadic generics would be convenient sometimes, but they usually can be substituted with macros or type-level recursion. GATs have reasonable alternatives in practice, too.

Orphan rules are not limitations or bugs of the type system. Those rules are necessary for generics to be sound. It looks to me like you are annoyed with the trait system because you are not able to derive some solutions yourself. That is fine, you can ask for help, but don't accuse the language of limitations and non-issues that are either necessities or aren't the fault of the language per se.

This is basically what C++ did, and it was apparent that this doesn't work well in practice. The very thing you are suggesting here is that one shall only use generics for the kind of thing you later discourage, e.g. putting a simple AsRef<[u8]> bound on a function. That is an extremely simplistic and harmfully naïve point of view. It is basically missing the point of a strong type (and trait) system.

The point of a strong type system is the ability to express complex domain constraints at compile time. The idea is that you should create such constraints that together, they make it impossible to compile code that violates domain logic.

You really shouldn't be actively discouraging others from this practice – suggesting that all a type system is for is checking that you didn't supply a string when a number was expected throws us right back to the '70s Pascal era. We can, and should, do better.

2 Likes

I didn't want to get confrontational about it, but you pretty much summed up my view on the matter. Of all the languages I've used so far, (which does not include languages like Haskel and friends which can prove theorems statically!) Rust's type+trait system is by far the most elegant and powerful I've come across. Especially in the near/distant feature when some of the straggling features stabilize. It's part of the reason I kept on learning it in the beginning.

1 Like

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.