ExamplesBy LevelBy TopicLearning Paths
422 Fundamental

422: Derive Macro Concepts

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "422: Derive Macro Concepts" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Many trait implementations are entirely mechanical: `Debug` for a struct just prints each field name and value, `Clone` copies each field, `PartialEq` compares each field. Key difference from OCaml: 1. **Integrated vs. plugins**: Rust's `Debug`, `Clone`, `PartialEq`, `Hash` are built into `rustc`; OCaml requires external ppx plugins in `dune` configuration.

Tutorial

The Problem

Many trait implementations are entirely mechanical: Debug for a struct just prints each field name and value, Clone copies each field, PartialEq compares each field. Writing these by hand for every type is tedious, error-prone (especially when fields are added later), and distracts from the actual logic. #[derive(Debug, Clone, PartialEq)] instructs the compiler to generate these mechanical implementations automatically based on the type's structure. Understanding what derive macros generate is essential for debugging unexpected behavior.

Derive macros are the most common form of code generation in Rust: serde::Deserialize, Debug, Clone, PartialEq, Hash, Default — virtually every struct uses them.

🎯 Learning Outcomes

  • • Understand what code #[derive(Debug)], #[derive(Clone)], and #[derive(PartialEq)] generate
  • • Learn how derive macros inspect the struct/enum structure to generate field-by-field code
  • • See the equivalence between ManualDebug's hand-written impl and the derived version
  • • Understand when derived implementations are insufficient (custom comparison, non-standard display)
  • • Learn the requirements: all fields must implement the derived trait
  • Code Example

    #![allow(clippy::all)]
    //! Derive Macro Concepts
    //!
    //! Understanding what derive macros generate.
    
    /// A point with derived traits.
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
    pub struct Point {
        pub x: i32,
        pub y: i32,
    }
    
    impl Point {
        pub fn new(x: i32, y: i32) -> Self {
            Point { x, y }
        }
    }
    
    /// Manual Debug implementation for comparison.
    pub struct ManualDebug {
        pub value: i32,
    }
    
    impl std::fmt::Debug for ManualDebug {
        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
            f.debug_struct("ManualDebug")
                .field("value", &self.value)
                .finish()
        }
    }
    
    /// Manual Clone implementation.
    impl Clone for ManualDebug {
        fn clone(&self) -> Self {
            ManualDebug { value: self.value }
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        use std::collections::HashSet;
    
        #[test]
        fn test_debug_derive() {
            let p = Point::new(1, 2);
            assert_eq!(format!("{:?}", p), "Point { x: 1, y: 2 }");
        }
    
        #[test]
        fn test_clone_derive() {
            let p1 = Point::new(3, 4);
            let p2 = p1.clone();
            assert_eq!(p1, p2);
        }
    
        #[test]
        fn test_copy_derive() {
            let p1 = Point::new(5, 6);
            let p2 = p1;
            let p3 = p1; // Still valid (Copy)
            assert_eq!(p2, p3);
        }
    
        #[test]
        fn test_hash_derive() {
            let mut set = HashSet::new();
            set.insert(Point::new(1, 1));
            set.insert(Point::new(1, 1)); // Duplicate
            assert_eq!(set.len(), 1);
        }
    
        #[test]
        fn test_default_derive() {
            let p = Point::default();
            assert_eq!(p, Point::new(0, 0));
        }
    
        #[test]
        fn test_manual_debug() {
            let m = ManualDebug { value: 42 };
            assert!(format!("{:?}", m).contains("42"));
        }
    }

    Key Differences

  • Integrated vs. plugins: Rust's Debug, Clone, PartialEq, Hash are built into rustc; OCaml requires external ppx plugins in dune configuration.
  • Trait vs. function: Rust derives implement traits (uniform interface); OCaml ppx generates standalone functions (show, equal, compare).
  • Field traversal: Both generate field-by-field code; Rust's version uses the trait interface (Debug::fmt per field), OCaml's uses pattern matching.
  • Error when field lacks trait: Rust compile error says "field x of type T doesn't implement Debug"; OCaml ppx gives similar errors.
  • OCaml Approach

    OCaml uses ppx_deriving or ppx_compare/ppx_hash from Jane Street for equivalent code generation. [@@deriving show, eq, ord] after a type definition generates show, equal, and compare functions. The show ppx generates pp functions for Format.formatter. Unlike Rust's integrated derive system, OCaml's derivers are separate ppx plugins that must be listed as build dependencies.

    Full Source

    #![allow(clippy::all)]
    //! Derive Macro Concepts
    //!
    //! Understanding what derive macros generate.
    
    /// A point with derived traits.
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
    pub struct Point {
        pub x: i32,
        pub y: i32,
    }
    
    impl Point {
        pub fn new(x: i32, y: i32) -> Self {
            Point { x, y }
        }
    }
    
    /// Manual Debug implementation for comparison.
    pub struct ManualDebug {
        pub value: i32,
    }
    
    impl std::fmt::Debug for ManualDebug {
        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
            f.debug_struct("ManualDebug")
                .field("value", &self.value)
                .finish()
        }
    }
    
    /// Manual Clone implementation.
    impl Clone for ManualDebug {
        fn clone(&self) -> Self {
            ManualDebug { value: self.value }
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        use std::collections::HashSet;
    
        #[test]
        fn test_debug_derive() {
            let p = Point::new(1, 2);
            assert_eq!(format!("{:?}", p), "Point { x: 1, y: 2 }");
        }
    
        #[test]
        fn test_clone_derive() {
            let p1 = Point::new(3, 4);
            let p2 = p1.clone();
            assert_eq!(p1, p2);
        }
    
        #[test]
        fn test_copy_derive() {
            let p1 = Point::new(5, 6);
            let p2 = p1;
            let p3 = p1; // Still valid (Copy)
            assert_eq!(p2, p3);
        }
    
        #[test]
        fn test_hash_derive() {
            let mut set = HashSet::new();
            set.insert(Point::new(1, 1));
            set.insert(Point::new(1, 1)); // Duplicate
            assert_eq!(set.len(), 1);
        }
    
        #[test]
        fn test_default_derive() {
            let p = Point::default();
            assert_eq!(p, Point::new(0, 0));
        }
    
        #[test]
        fn test_manual_debug() {
            let m = ManualDebug { value: 42 };
            assert!(format!("{:?}", m).contains("42"));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
        use std::collections::HashSet;
    
        #[test]
        fn test_debug_derive() {
            let p = Point::new(1, 2);
            assert_eq!(format!("{:?}", p), "Point { x: 1, y: 2 }");
        }
    
        #[test]
        fn test_clone_derive() {
            let p1 = Point::new(3, 4);
            let p2 = p1.clone();
            assert_eq!(p1, p2);
        }
    
        #[test]
        fn test_copy_derive() {
            let p1 = Point::new(5, 6);
            let p2 = p1;
            let p3 = p1; // Still valid (Copy)
            assert_eq!(p2, p3);
        }
    
        #[test]
        fn test_hash_derive() {
            let mut set = HashSet::new();
            set.insert(Point::new(1, 1));
            set.insert(Point::new(1, 1)); // Duplicate
            assert_eq!(set.len(), 1);
        }
    
        #[test]
        fn test_default_derive() {
            let p = Point::default();
            assert_eq!(p, Point::new(0, 0));
        }
    
        #[test]
        fn test_manual_debug() {
            let m = ManualDebug { value: 42 };
            assert!(format!("{:?}", m).contains("42"));
        }
    }

    Deep Comparison

    OCaml vs Rust: derive macro concept

    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

  • Expand and study: Add #[allow(unused)] and a new field pub label: Option<String> to Point. Predict what the derived Debug output will look like, then verify with format!("{:?}", p). Add a field of a type that doesn't implement Debug and study the compile error.
  • Custom debug: Implement a struct Password(String) where the derived Debug would expose the secret. Write a custom Debug that outputs Password("***") regardless of the actual value.
  • Partial derive: Implement a struct where PartialEq should compare only some fields (e.g., a User where equality is by id only). Write the manual implementation and add a comment explaining why the derived version would be wrong.
  • Open Source Repos