ExamplesBy LevelBy TopicLearning Paths
424 Advanced

424: Proc Macro Derive

Functional Programming

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

  • • Understand the derive proc macro lifecycle: TokenStream in → parse with syn → generate with quote!TokenStream out
  • • Learn how syn::DeriveInput represents parsed struct definitions with field names and types
  • • See how quote::quote! generates code with token interpolation using #variable syntax
  • • Understand the crate separation requirement: proc macros in proc-macro = true crates
  • • Learn how #[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

  • Registration: Rust registers via #[proc_macro_derive(Name)]; OCaml uses Ppx_deriving.register "name" (module Deriver).
  • Code generation: Rust uses quote! with # interpolation; OCaml uses Ast_builder with explicit AST node construction.
  • Error location: Rust can use span from syn for precise error locations; OCaml uses Location.t values from the AST.
  • Testing: Rust tests proc macros with 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);
        }
    }
    ✓ Tests Rust test suite
    #[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

  • Rust macros operate at compile time
  • OCaml uses ppx for similar metaprogramming
  • Both languages support powerful code generation
  • Rust's macro_rules! is built into the language
  • OCaml's approach requires external tooling
  • Exercises

  • Describe trait: Implement a #[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.
  • Getters derive: Write #[derive(Getters)] generating pub fn field_name(&self) -> &FieldType for every field. Handle pub and private fields, skipping fields with #[getter(skip)] attribute.
  • Builder derive: Implement #[derive(Builder)] that generates a {StructName}Builder with setter methods and a build() method. Handle Option<T> fields as optional, other fields as required.
  • Open Source Repos