Auto-deriving combined traits

Hello! I'm curious if there's a good way to automatically derive an existing combined trait. For example, I got started with wanting to combine several traits that I'm using into one big one, which involved going from having to mark every generic T with each trait i.e. T: Debug + Clone + Serialize + Deserialize. To do that, I looked into creating a combined trait (call it MyTrait) using

pub trait MyTrait: Debug + Clone + Serialize + Deserialize {}
impl<T> MyTrait for T where T: Debug + Clone + Serialize + Deserialize {}

where I'm importing serde prior to building this. And that compiles great, and I'm able to use that internally to cut down on the verbosity of my code as long as external structs (i.e. for users of my library), write something like

#[derive(Debug, Clone, Serialize, Deserialize)
struct MyStruct {x: f32, y: f32}

Ideally, I'd love to just be able to do a #[derive(MyTrait)], but now when I do so, I get an error message that looks like

error: cannot find derive macro `MyTrait` in this scope
 --> examples/my_example.rs:8:10
  |
8 | #[derive(MyTrait)]
  |          ^^^
  |
note: `MyTrait` is imported here, but it is only a trait, without a derive macro

It almost feels like this is a case where it should be auto-magically determined by the compiler that since MyTrait is a composite of other traits, and those other traits can be validly derived on MyStruct, that MyStruct can have MyTrait derived on it without needing it's own explicit derive macro.

Candidly, part of the issue is that I'm not very comfortable with macros at the moment; if there's not a simple solution, I'm willing to bite the bullet and really dig into that documentation, but if anyone has suggestions for how I can accomplish this sort of thing without needing to jump headfirst down this rabbithole (especially since I'm using a re-exported serde trait, which I would imagine will complicate things), I'd really appreciate it. Thanks for your consideration :slight_smile:

This is only true if MyTrait has no methods, and even then, it requires type-level computation. Macros are expanded way before type checking even begins, so it's impossible to manipulate derive macros based on type-level relationships.

It wouldn't be a good idea anyway. Action at a distance is generally discouraged in Rust because it hurts maintenability and readability. If your trait requires 4 supertraits, let the user derive all 4 or 0, or 1, or 2 or 3 of them, as required by each particular type.

Yes, MyTrait has no methods--it's basically solely there as an ergonomic way to limit the verbosity of the code.

I'm not sure if this means it's actually impossible, or if it just means that the error would be thrown further down the line in the compilation process. One could really easily imagine a string replacement process in a build.rs script that does this--everywhere it sees #[derive(MyTrait)], it would simply replace that with #[derive(Debug, Clone, Serialize, Deserialize)], and then any errors based on the incompatibility of these traits with the the types it's attempting to be derived on would show up at the same point as if you'd tried to derive it normally. I was under the impression that this is basically was a macro does, except those don't need to rely on a build script, and can just make it a standard part of the compilation process when used as a dependency in other people's code. Is that bad assumption?

I'm open to other arguments, but I'm not sure that I agree with this. "Action at a distance" is basically another term of "abstraction," which is sort of the point. I don't really want users to care about the underlying specific traits, or I want to be able to update those in my library (i.e. add in a PartialEq trait) without most users needing to care about the underlying structure of MyTrait that they're using. Or if they've somehow written a struct that can't automatically have that PartialEq derived, they can manually solve for that trait instead, which is they'd have needed to do anyway.

If this were an automatic type of thing, then deriving Eq would derive PartialEq and deriving Copy would derive Clone. You can read some difficulties in implementation and arguments for and against doing so in the postponed RFC 2385.

2 Likes

I appreciate this link! It sent me down a bit of reading rabbit-hole, and it was really interesting to see the kind of discussion about this in the past. I was thinking pretty similarly along the lines as in this comment in that thread. It seems like this is feasibly possible, but not actually implemented due to potential breaking changes. It does seem like it's popped up a number of times in the past, however (like this), so I guess it's just something I'll have to keep an eye on to see if anything changes in the future. A little disappointing, but the actual number of trait derives needed for my particular case isn't too crazy, so this isn't hugely problematic. Still, something I'll be keeping an eye on over the next couple years--who knows, maybe someone will put in a new RFC about it for the 2024 edition :slight_smile: Thanks for your time!

Doing the replacement is not the part that requires typechecking, of course. The part that requires typechecking is knowing what 4 traits MyTrait is supposed to expand to in the first place.

I'm not here to argue semantics, but that's definitely not how I (and many others) use the term. I'm not against abstraction, but I am against too much magic. Supertrait relationships are part of your interface whether you like it or not, so they can't and shouldn't just be ignored.

I also prefer not to argue semantics, and it just feels like we're talking past one another to a degree. Your initial comment seemed to be based on the "should vs. should not", and not "can vs. can not". While I appreciate your viewpoint on the value of the trade-offs of such a feature, I think we've arrived at different conclusions: if Rust allowed the derivation of a super trait in such as way that the process automatically/implicitly included the derivation of all traits of which it was comprised (unless they already had a manual implementation, etc.), I would choose to use that feature. Based on the threads linked in above comments, interest in this sort of feature has not been not unique to me, including other members of the community whose language knowledge certainly exceeds my own.

My primary question was if this was actually possible; it appears the answer is no, as doing so would be a breaking change in any current Rust edition, and potentially could lead to a conflict via the orphan rule depending on how it was implemented.

I think there might be some confusion here about how derive macros work.

Derive macros don't actually have any knowledge of traits. The fact that derive macros are named the same as the traits they derive is just a convention, the derive macro system doesn't care, and can't actually check that. It's also entirely possible to write a derive macro that doesn't implement a trait at all.

So there would need to be some sort of bridge between your trait definition and the procedural macro system in order to get what you wanted.

That RFC is as far as I can tell, only tangentially related to your use case. The future directions mentions a possible extension that would allow a derive macro to imply several other derives. You could use that to do what you wanted by creating an empty derive macro that implied the derives you wanted.

I don't see any part of that RFC which would enable any super trait derives to be automatically inferred based on the trait definition alone. My impression is that the proposal was to implement this behavior as part of the appropriate existing derive macros, not via a language feature that would add this functionality automatically.

Apologies if you already knew all of that!

2 Likes

The relation is in the other direction. If the OP's intuition was a thing...

It almost feels like this is a case where it should be auto-magically determined by the compiler that since MyTrait is a composite of other traits, and those other traits can be validly derived on MyStruct, that MyStruct can have MyTrait derived on it without needing it's own explicit derive macro.

...then that RFC would be implemented (even if restricted to itemless traits, implemented for Copy and Eq). Thus, in my estimation (given the conversation in and postponement of that RFC), chances are low it would ever happen as an automatic thing (no matter how it was implemented).

Opt-in, maybe. (But probably not without the ability to derive the same trait multiple times or otherwise omit overlapping transitive derives; imagine how many custom traits would overlap in dependencies, not to mention the desire to see that traits you care about are derived by explicitly mentioning them even if transitively implied.)

3 Likes