What traits should I normally derive?

I'm a beginner playing around with the language, and so far I've mostly just been adding traits when the compiler complains that it needs them, which leads to each type having a haphazard collection of traits.

I assume when you make a library you want to be more consistent about it, so you don't end up frustrating users by making them unable to e.g. print out debug values, or use your enum in a hash map.
So I'm looking for guidelines on when you should derive what traits.

The code in the rustc repository isn't very consistent about this at a glance either.
Result covers most bases:

#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]

While for instance in test, ColorConfig derives only Copy, while TestResult derives Clone and PartialEq.

As a concrete example, say I had a simple public enum like this:

pub enum Answer {
    Yes,
    No,
    Maybe,
}

What traits should I be deriving? Should it have both Clone and Copy? Eq or PartialEq?

4 Likes

This is a great question. I'll get it started with some of my own opinions. I'm sure others will chime in!

Note that this advice is assuming you're developing a library and that the type in question is a public type exposed to users of your library.

I think some good general advice is be conservative because the traits you derive on your types are part of its public interface. For example, if you derive Copy on an enum but later decide to, say, add a variant with a String in it, then you're forced to remove the impl for Copy on your type. This will be a breaking change for downstream users. This is true for any other trait, but Copy can be especially tricky because it's easy to change the definition of your type such that it can no longer be Copy.

Being conservative is a good thing here because adding an impl to your type can never be a breaking change. (Is this true? Can someone check me on that? I think it is.) IMO, the process of adding impls "because the compiler says you needed them" is not a bad place to start. It keeps your impls to the minimum possible. If your types need more impls, then someone will hopefully come along and say, "Hey you should derive this trait on your type because such and such..." And then you can evaluate whether it's worthwhile to do so, or whether the user should just create a wrapper type and derive the impl themselves (which may not be possible if the impl requires details of the type that aren't exposed).

Deriving Debug is probably close to a universally good thing. It's just plain convenient to allow users of your library to dump a quick 'n' dirty representation of a value. Moreover, the name of the trait itself won't give any false impressions about whether that representation should be shown to the user.

2 Likes

The only way I can think of that adding an impl can break someone's code is by making a method call ambiguous, forcing them to change it to UFCS. This seems like it would be both uncommon (especially for the traits you can derive) and relatively minor as far as breaking changes go; I'd imagine that it would be fairly easy to create a tool which automates this transformation when a conflict is first introduced.

If negative trait bounds are added then I can see that being a problem as well, though I suspect they'd really only be used for specialization anyways so it shouldn't really be a problem.

Not implementing Eq when there's PartialEq has a particular meaning: the equality relation is not total. An example is floats where NaN != NaN.

I find it annoying that Clone needs to be explicitly derived for all types that implement Copy. Is there ever a case when a bit-copyable object needs to have a non-trivial Clone implementation?

2 Likes

I have a feeling derive will eventually be upgraded to understand certain "obvious" things like Copy -> Clone and Eq -> PartialEq.

Or even better, a blanket impl: impl<T: Copy> Clone for T { fn clone(&self) -> T { *self } }

This was one of the big motivations for multiple/conditional dispatch. I wonder what happened to it.

3 Likes

It seems to me that there should be a macro that implements all of the common traits, just like Derive, but with possibility of exceptions. This should be opt-out, not opt-in.

Something I've been wondering related to this is why it's necessary for types to explicitly implement/derive from Debug. Does it add extra code which increases the size of the compiled binary? I come from a scripting language background and it's annoying to have to manually mark every type I want to be able to print for quick debugging. Just wondering the reason behind it.

Thanks for the answers.

So as I understand it so far the rules of thumb are:

  • Always implement Debug
  • Don't derive PartialEq if you might add a noncomparable field later
  • Don't derive Eq if you might add a float field later
  • Don't derive Copy if you might add a non-copyable field later. This includes a lot of common types like String

And so on.

So if I'm sure a type will stay a simple enum, like my example, I guess I should just go ahead and derive every trait possible.

I've created some types that would produce ugly and/or uninformative output with compiler-derived Debug. Something that contains raw pointers would be formatted better if the pointer is used in some way to obtain human-readable data.

I think a better rule of thumb is to only add these traits if they make clear sense for the type as a whole. Copy is stated intent to keep the values of the type copyable, so its contents are not expected to allocate or mutably borrow data. If the meaning of == on the type is not obvious, perhaps it should not implement PartialEq, and instead provide access to members or derived values that can be tested for equality.

Eq is more than just "no floats". You need to understand the rules on total equality to apply it correctly.

You may say that, but "make clear sense for the type as a whole" isn't much of a rule of thumb for me as a beginner.

I don't know yet what parts of the standard library take Eq but not PartialEq, for instance, so it's hard for me know if not implementing it makes things harder for users.

This is the kind of things that I hope will get condensed into best practices, so even beginners can write reasonable code without experiencing the pain points of mistakenly adding/not adding traits themselves.

1 Like