🦀 Tip of the day 26: Derive Macros, or How to Make the Compiler Write Your Boilerplate
You have a struct. You want to print it for debugging. You write the natural thing:
#![allow(unused)]
fn main() {
struct Player {
name: String,
score: u32,
}
let p = Player { name: String::from("Alice"), score: 42 };
println!("{:?}", p); // ERROR: `Player` doesn't implement `Debug`
}
The compiler complains: Player does not implement the Debug trait. You can write that implementation yourself, but for a “print every field” version, the compiler will write it for you. Just ask:
#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Player {
name: String,
score: u32,
}
let p = Player { name: String::from("Alice"), score: 42 };
println!("{:?}", p); // Player { name: "Alice", score: 42 }
}
#[derive(...)] is an attribute that asks the compiler to generate a standard implementation for one or more traits. Each name inside the parentheses is a trait. The compiler reads your struct and writes the impl for you.
The standard derives you will use most often:
Debug: enables{:?}formatting inprintln!and friends.Clone: gives your type a.clone()method. It calls.clone()on each field in turn, so the semantics depend on what each field’sCloneimpl does.Copy: opts into copy-on-assignment (tip 19). Only legal when every field is alsoCopy.PartialEq: enables==and!=comparisons.Eq: a marker trait paired withPartialEq, asserting equality is reflexive (x == xalways holds). No methods, no runtime cost. Required alongsideHashforHashMapkeys.Default: gives youType::default(), returning the default value (zero for numbers, empty for collections).Hash: lets the type be a key inHashMapandHashSet.PartialOrd: enables<,<=,>,>=operators. Can answer “less”, “equal”, “greater”, or “incomparable”, and that last case is whyf32andf64only implement this one (NaNis incomparable with anything, including itself).Ord: a total ordering: any two values compare to a definite answer. Required for sorting and forBTreeMap/BTreeSetkeys.Ordis a sub-trait ofPartialOrdandEq, so the four typically travel together:#[derive(PartialEq, Eq, PartialOrd, Ord)].
You can stack them:
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, Default)]
struct Player {
name: String,
score: u32,
}
}
A note on what derive macros actually are. They are not the same machinery as the ! macros from tips 1 and 2. Those are declarative macros: pattern substitution on tokens.
#[derive(...)] is a procedural macro. Proc macros are real Rust code that runs at compile time, takes your syntax as input, and produces new syntax as output. They come in three flavors:
- Function-like (
my_macro!(...)): used the same way as declarative macros, but written as proc-macro code. Example:sqlx::query!(...). - Attribute (
#[my_attr]): can rewrite the entire item it is attached to. Example:#[tokio::main]. - Derive (
#[derive(MyTrait)]): can only add newimplblocks. It never modifies the item itself, which is why derives feel safe to read.
The standard derives are built into the compiler. Custom ones come from external crates: serde::Serialize for JSON serialization, thiserror::Error for error types, and so on.
You do not need to know how to write any flavor to use them. You write the attribute, the compiler runs the macro, the impl appears.
A few things worth knowing about the standard derives (custom derives from crates follow similar conventions but can set their own rules):
- Every field must implement the trait you are deriving.
#[derive(Debug)]on a struct with a non-Debugfield is a compile error. Copycannot be derived withoutClone.Copyis a sub-trait ofClone, so the pair always travels together.- Derives apply to enums as well as structs. The standard ordering and equality derives compare variants by tag first, then by field.
- For
PartialOrdandOrd, the comparison is lexicographic in the order the fields are declared. Ifscoreshould dominatename, declarescorefirst. - To see what was generated by any derive (standard or custom), install
cargo-expandand runcargo expand. It prints your code with all derives expanded inline.
The takeaway: #[derive(...)] asks the compiler to write a standard implementation of a trait for you. Most Rust structs have at least #[derive(Debug)], and many have Clone, PartialEq, Default alongside.
🔇 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Error)] fits on one line. Whether it should is a separate conversation.