Skip to content

← Shape Your Data: Structs, Enums, Pattern Matching step 5 of 26

Easy Primitives

Deriving traits, and when derive gets the bounds wrong

Report the default value of named fields on a Settings struct.

pub fn field_defaults(names: Vec<String>) -> Vec<String>

For each name, return retries as a plain number, verbose as true/false, label with {:?} (so an empty string shows up as ""), and "?" for any name that is not a field.

The starter compiles and passes every test, and it still fails. That is the lesson. It ships a hand-written impl Default for Settings whose body is, line for line, exactly what #[derive(Default)] would have generated. clippy notices:

error: this `impl` can be derived

Your job is to delete the hand-written impl and put Default in the derive list. Roughly ten lines of code disappear.

What derive actually is

#[derive(Debug, Clone, PartialEq)] is a macro that reads your type definition and writes trait implementations for it. Nothing more. The code it writes is ordinary Rust that you could have typed yourself, and — this is the part that matters — you own what it generates. It is not a compiler primitive you can wave at, it is generated source with consequences.

The std traits you can derive: Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default. That is the whole list. Notably Display is deliberately not derivable — there is no single obviously correct way to show a type to a human, so std makes you decide.

Derived Default gives each field that field’s default: 0 for integers, false for bool, "" for String, None for Option<T>, an empty Vec. On an enum you must nominate a variant with #[default].

Where the abstraction leaks: derive’s bounds

Here is the thing almost nobody tells beginners, and the source of a whole family of baffling E0277s later on.

Derive adds a bound for every type parameter, whether or not it is needed.

#[derive(Clone)]
struct Wrap<T>(std::rc::Rc<T>);

The generated impl is impl<T: Clone> Clone for Wrap<T>. But Rc<T> is Clone for every T — cloning an Rc bumps a refcount and never touches the T at all. So Wrap<NotClonable> could be Clone, and thanks to derive it is not. Writing the impl by hand fixes it:

impl<T> Clone for Wrap<T> {
    fn clone(&self) -> Self { Wrap(self.0.clone()) }
}

Same story for PartialEq on a PhantomData<T> field, Debug on a type whose T is only ever used behind a reference-counted pointer, and so on. When a generic type mysteriously “does not implement Clone“ and you can see no reason why, this is usually the reason.

It is also why the derivable_impls lint that grades this problem deliberately switches itself off for generic types: on a generic type the hand-written impl and the derived one may genuinely differ, so clippy will not tell you they are the same. On a plain concrete struct like Settings there is no such escape, and the lint is right.

Two more lints in this family

expl_impl_clone_on_copy — if a type is Copy, writing Clone by hand is suspicious, because the only correct Clone for a Copy type is *self.

derive_partial_eq_without_eq — a type that could be Eq but only derives PartialEq is usually an oversight. (Only “usually”: floats are the famous exception, and they get an item of their own later in this track.)

Remember the grade is compile + tests + clippy -D warnings.