424: Proc Macro Derive
Tutorial Video
Text description (accessibility)
This video demonstrates the "424: Proc Macro Derive" functional Rust example. Difficulty level: Advanced. Key concepts covered: Functional Programming. A custom derive macro generates trait implementations automatically from a type's definition. Key difference from OCaml: 1. **Registration**: Rust registers via `#[proc_macro_derive(Name)]`; OCaml uses `Ppx_deriving.register "name" (module Deriver)`.
Tutorial
The Problem
A custom derive macro generates trait implementations automatically from a type's definition. When you annotate #[derive(MyTrait)], the proc macro receives the struct/enum definition as a token stream, parses it to find field names and types, and emits an impl MyTrait for TheType block. This is the mechanism behind serde::Deserialize — it inspects every field name and type, generating JSON deserialization code specific to that struct's shape, something impossible with macro_rules!.
Custom derive macros appear whenever library authors want users to opt-in to generated boilerplate: ORMs generating SQL mappings, test frameworks generating fixtures, protocol libraries generating serialization code.
🎯 Learning Outcomes
TokenStream in → parse with syn → generate with quote! → TokenStream outsyn::DeriveInput represents parsed struct definitions with field names and typesquote::quote! generates code with token interpolation using #variable syntaxproc-macro = true crates#[proc_macro_derive(Name)] registers the macro for use with #[derive(Name)]Code Example
#![allow(clippy::all)]
//! Derive Macro Patterns
//!
//! Common patterns for derive macros.
/// Simulating what derive macros generate.
/// A simple newtype for demonstration.
pub struct Meters(pub f64);
/// What #[derive(Add)] might generate:
impl std::ops::Add for Meters {
type Output = Meters;
fn add(self, other: Meters) -> Meters {
Meters(self.0 + other.0)
}
}
/// What #[derive(From)] might generate for newtype:
impl From<f64> for Meters {
fn from(v: f64) -> Meters {
Meters(v)
}
}
/// What #[derive(Into)] provides automatically with From:
impl From<Meters> for f64 {
fn from(m: Meters) -> f64 {
m.0
}
}
/// Example enum for dispatch.
pub enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
}
impl Shape {
pub fn area(&self) -> f64 {
match self {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_meters_add() {
let a = Meters(1.0);
let b = Meters(2.0);
let c = a + b;
assert_eq!(c.0, 3.0);
}
#[test]
fn test_meters_from() {
let m: Meters = 5.0.into();
assert_eq!(m.0, 5.0);
}
#[test]
fn test_meters_into() {
let m = Meters(10.0);
let f: f64 = m.into();
assert_eq!(f, 10.0);
}
#[test]
fn test_circle_area() {
let s = Shape::Circle { radius: 1.0 };
assert!((s.area() - std::f64::consts::PI).abs() < 0.001);
}
#[test]
fn test_rectangle_area() {
let s = Shape::Rectangle {
width: 3.0,
height: 4.0,
};
assert_eq!(s.area(), 12.0);
}
}Key Differences
#[proc_macro_derive(Name)]; OCaml uses Ppx_deriving.register "name" (module Deriver).quote! with # interpolation; OCaml uses Ast_builder with explicit AST node construction.span from syn for precise error locations; OCaml uses Location.t values from the AST.trybuild crate (compile-fail tests); OCaml uses ppx_deriving's test infrastructure.OCaml Approach
OCaml's ppx_deriving library provides the framework for writing custom derivers. A deriver registers with Ppx_deriving.register providing type_decl -> structure_item list functions. The OCaml AST types (type_declaration, label_declaration) correspond to syn::DeriveInput and syn::Field. Code generation uses Ast_builder.Default module functions rather than quote!.
Full Source
#![allow(clippy::all)]
//! Derive Macro Patterns
//!
//! Common patterns for derive macros.
/// Simulating what derive macros generate.
/// A simple newtype for demonstration.
pub struct Meters(pub f64);
/// What #[derive(Add)] might generate:
impl std::ops::Add for Meters {
type Output = Meters;
fn add(self, other: Meters) -> Meters {
Meters(self.0 + other.0)
}
}
/// What #[derive(From)] might generate for newtype:
impl From<f64> for Meters {
fn from(v: f64) -> Meters {
Meters(v)
}
}
/// What #[derive(Into)] provides automatically with From:
impl From<Meters> for f64 {
fn from(m: Meters) -> f64 {
m.0
}
}
/// Example enum for dispatch.
pub enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
}
impl Shape {
pub fn area(&self) -> f64 {
match self {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_meters_add() {
let a = Meters(1.0);
let b = Meters(2.0);
let c = a + b;
assert_eq!(c.0, 3.0);
}
#[test]
fn test_meters_from() {
let m: Meters = 5.0.into();
assert_eq!(m.0, 5.0);
}
#[test]
fn test_meters_into() {
let m = Meters(10.0);
let f: f64 = m.into();
assert_eq!(f, 10.0);
}
#[test]
fn test_circle_area() {
let s = Shape::Circle { radius: 1.0 };
assert!((s.area() - std::f64::consts::PI).abs() < 0.001);
}
#[test]
fn test_rectangle_area() {
let s = Shape::Rectangle {
width: 3.0,
height: 4.0,
};
assert_eq!(s.area(), 12.0);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_meters_add() {
let a = Meters(1.0);
let b = Meters(2.0);
let c = a + b;
assert_eq!(c.0, 3.0);
}
#[test]
fn test_meters_from() {
let m: Meters = 5.0.into();
assert_eq!(m.0, 5.0);
}
#[test]
fn test_meters_into() {
let m = Meters(10.0);
let f: f64 = m.into();
assert_eq!(f, 10.0);
}
#[test]
fn test_circle_area() {
let s = Shape::Circle { radius: 1.0 };
assert!((s.area() - std::f64::consts::PI).abs() < 0.001);
}
#[test]
fn test_rectangle_area() {
let s = Shape::Rectangle {
width: 3.0,
height: 4.0,
};
assert_eq!(s.area(), 12.0);
}
}
Deep Comparison
OCaml vs Rust: proc macro derive
See example.rs and example.ml for side-by-side implementations.
Key Points
Exercises
#[derive(Describe)] that generates impl Describe for T { fn describe() -> String { "T { field1: type1, field2: type2 }" } } using field names and type names from syn. Test it on a two-field struct.#[derive(Getters)] generating pub fn field_name(&self) -> &FieldType for every field. Handle pub and private fields, skipping fields with #[getter(skip)] attribute.#[derive(Builder)] that generates a {StructName}Builder with setter methods and a build() method. Handle Option<T> fields as optional, other fields as required.