422: Derive Macros: Concept and Usage
Difficulty: 3 Level: Advanced Demystify `#[derive(Debug)]` by showing what code it actually generates โ and understand when to use derive vs write implementations by hand.The Problem This Solves
Every Rust beginner writes `#[derive(Debug, Clone, PartialEq)]` and it magically works. But when something goes wrong โ a custom type doesn't derive, a newtype wraps a non-`Hash` type, an enum variant breaks `Ord` ordering โ you're stuck. You can't debug what you don't understand. Beyond debugging, derive macros are the entry point to a deeper capability: procedural macros. `#[derive(Serialize)]` from serde, `#[derive(Error)]` from thiserror, `#[derive(Component)]` from Bevy โ all of these are proc macros that generate substantial code from a struct definition. Understanding what `#[derive(Debug)]` actually emits teaches you to reason about what any derive macro might emit. The manual equivalents also matter in practice. You'll hand-write `PartialEq` when two structs are equal only when a subset of fields match, or `Debug` when you want to redact a password field. Knowing what derive would have generated makes writing the custom version straightforward.The Intuition
`#[derive(Debug)]` is a code generator. Before compilation, the Rust compiler hands the macro your struct definition and says "generate the `Debug` impl." The macro produces something like what you'd write yourself โ `f.debug_struct("Point").field("x", &self.x).field("y", &self.y).finish()` โ and that generated code is compiled alongside your own. All standard derive traits follow the same pattern: they inspect each field or variant and compose the implementation recursively. `#[derive(Clone)]` calls `.clone()` on each field. `#[derive(PartialEq)]` compares fields pairwise. `#[derive(Ord)]` compares fields left to right, using the first non-equal result.How It Works in Rust
// What you write:
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
struct Point { x: i32, y: i32 }
// What #[derive(Debug)] generates (simplified):
impl fmt::Debug for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Point")
.field("x", &self.x) // calls Debug on each field
.field("y", &self.y)
.finish()
}
}
// What #[derive(PartialEq)] generates:
impl PartialEq for Point {
fn eq(&self, other: &Self) -> bool {
self.x == other.x && self.y == other.y
}
}
// What #[derive(Default)] generates:
impl Default for Point {
fn default() -> Self {
Point { x: i32::default(), y: i32::default() }
}
}
Practical outcomes of derived traits:
- `Debug` โ `{:?}` formatting and `dbg!()` macro
- `Hash + Eq` โ use as `HashMap` key
- `Ord` โ `.sort()` on `Vec<T>`
- `Clone` โ `.clone()` method
- `Default` โ `..Default::default()` struct update syntax
What This Unlocks
- Custom derives for your own types โ understanding the pattern leads directly to writing proc macros with `syn` + `quote` that generate any code from a struct definition.
- Knowing when NOT to derive โ hand-write `Debug` to redact secrets, hand-write `PartialEq` for semantic equality (two `HashMap`s with same entries but different capacities are equal).
- Third-party derives โ `serde::Serialize`, `thiserror::Error`, `clap::Parser` all follow this same expansion model; you can reason about what they generate.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Code generation from type | `ppx_deriving` (show, eq, ord) โ third-party, requires opam pkg | `#[derive(...)]` โ built into compiler, no deps for std traits |
| Manual equivalent | Write `show_point`, `equal_point` functions | Implement `fmt::Debug`, `PartialEq` traits |
| Syntax | `[@@deriving show, eq]` attribute | `#[derive(Debug, PartialEq)]` attribute |
| Ordering | `compare` polymorphic function | `PartialOrd` + `Ord` traits; field order matters for derived `Ord` |
| Hash map key | Any type with structural equality | Requires `Hash + Eq` both derived or both implemented |
// Derive macros: concept and usage in Rust
// All standard derivable traits
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
struct Point {
x: i32,
y: i32,
}
// Enum derivations
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum Shape {
Circle { radius: u32 },
Rectangle { width: u32, height: u32 },
Triangle { base: u32, height: u32 },
}
// What derive actually generates โ shown manually for Point
// (This is equivalent to #[derive(Debug)] for Point)
mod manual_impls {
use super::Point;
use std::fmt;
// What #[derive(Debug)] generates:
impl fmt::Debug for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Point")
.field("x", &self.x)
.field("y", &self.y)
.finish()
}
}
}
// Using derived traits in practice
use std::collections::{HashMap, HashSet, BTreeSet};
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 1, y: 2 };
let p3 = Point { x: 3, y: 4 };
// Debug
println!("{:?}", p1);
println!("{:#?}", p1); // pretty-print
// PartialEq / Eq
println!("p1 == p2: {}", p1 == p2);
println!("p1 == p3: {}", p1 == p3);
// Ord โ sorting
let mut points = vec![p3.clone(), p1.clone(), p2.clone()];
points.sort();
println!("Sorted: {:?}", points);
// Hash โ use as HashMap key
let mut map: HashMap<Point, String> = HashMap::new();
map.insert(p1.clone(), "origin-ish".to_string());
println!("Map lookup: {:?}", map[&p1]);
// Clone
let p4 = p1.clone();
println!("Cloned: {:?}", p4);
// Default
let default_p = Point::default();
println!("Default: {:?}", default_p);
// Enum derive
let shapes = vec![
Shape::Circle { radius: 5 },
Shape::Rectangle { width: 3, height: 4 },
];
let mut btree: BTreeSet<Shape> = shapes.into_iter().collect();
btree.insert(Shape::Triangle { base: 3, height: 4 });
println!("Shapes (sorted): {:?}", btree);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_derived_eq() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 1, y: 2 };
assert_eq!(p1, p2);
}
#[test]
fn test_derived_ord() {
let mut v = vec![Point { x: 3, y: 0 }, Point { x: 1, y: 0 }];
v.sort();
assert_eq!(v[0], Point { x: 1, y: 0 });
}
#[test]
fn test_derived_clone() {
let p = Point { x: 5, y: 6 };
let q = p.clone();
assert_eq!(p, q);
}
#[test]
fn test_derived_default() {
let p = Point::default();
assert_eq!(p.x, 0);
assert_eq!(p.y, 0);
}
}
(* Derive macro concepts in OCaml *)
(* OCaml show and eq from ppx_deriving *)
(* Without ppx: manual implementations *)
type point = { x: float; y: float }
(* Manual "derive Debug" *)
let show_point {x; y} = Printf.sprintf "Point { x = %g; y = %g }" x y
(* Manual "derive Eq" *)
let equal_point a b = a.x = b.x && a.y = b.y
(* Manual "derive Ord" *)
let compare_point a b =
let cx = compare a.x b.x in
if cx <> 0 then cx else compare a.y b.y
(* With ppx_deriving, you'd write: *)
(* type point = { x: float; y: float } [@@deriving show, eq, ord] *)
type shape = Circle of float | Rectangle of float * float | Triangle of float * float * float
let show_shape = function
| Circle r -> Printf.sprintf "Circle(%g)" r
| Rectangle (w, h) -> Printf.sprintf "Rectangle(%g, %g)" w h
| Triangle (a, b, c) -> Printf.sprintf "Triangle(%g, %g, %g)" a b c
let () =
let p1 = {x = 1.0; y = 2.0} in
let p2 = {x = 1.0; y = 2.0} in
let p3 = {x = 3.0; y = 4.0} in
Printf.printf "%s\n" (show_point p1);
Printf.printf "p1 = p2: %b\n" (equal_point p1 p2);
Printf.printf "p1 = p3: %b\n" (equal_point p1 p3);
Printf.printf "compare: %d\n" (compare_point p1 p3);
List.iter (fun s -> Printf.printf "%s\n" (show_shape s))
[Circle 5.0; Rectangle 3.0 4.0; Triangle 3.0 4.0 5.0]