422: Derive Macro Concepts
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
#[derive(Debug)], #[derive(Clone)], and #[derive(PartialEq)] generateManualDebug's hand-written impl and the derived versionCode 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
Debug, Clone, PartialEq, Hash are built into rustc; OCaml requires external ppx plugins in dune configuration.show, equal, compare).Debug::fmt per field), OCaml's uses pattern matching.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"));
}
}#[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
Exercises
#[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.struct Password(String) where the derived Debug would expose the secret. Write a custom Debug that outputs Password("***") regardless of the actual value.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.