๐Ÿฆ€ Functional Rust

424: Custom #[derive(MyTrait)]

Difficulty: 4 Level: Expert Generate trait implementations automatically for any struct or enum โ€” the same mechanism powering `#[derive(Debug, Serialize, Clone)]`.

The Problem This Solves

Some traits have an obvious mechanical implementation that depends only on a type's structure. `Debug` prints fields. `Serialize` iterates them. `Hash` combines field hashes. Writing these by hand for every new struct is tedious, error-prone (forget to update when adding a field), and produces hundreds of lines of boilerplate. Custom derive macros solve this: annotate your struct with `#[derive(YourTrait)]` and the macro reads the struct's fields at compile time, generates the implementation, and inserts it as if you'd written it by hand. The generated code is visible via `cargo expand`, it's zero-overhead, and it stays in sync โ€” add a field and the derive regenerates. This is the mechanism the entire Rust ecosystem is built on. `serde`'s `Serialize`/`Deserialize`, `thiserror`'s `Error`, `clap`'s `Parser` โ€” all derive macros. Learning to write one makes you fluent in how the ecosystem actually works.

The Intuition

A derive macro reads your struct's fields and generates an `impl YourTrait for YourStruct` block at compile time โ€” like a code generator that runs every time you build.

How It Works in Rust

// In your proc-macro crate:
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data, Fields};

#[proc_macro_derive(Describe)]
pub fn derive_describe(input: TokenStream) -> TokenStream {
 let input = parse_macro_input!(input as DeriveInput);
 let name = &input.ident;  // struct name as Ident

 // Extract named field names
 let fields = match &input.data {
     Data::Struct(s) => match &s.fields {
         Fields::Named(f) => f.named.iter()
             .map(|f| &f.ident)
             .collect::<Vec<_>>(),
         _ => vec![],
     },
     _ => panic!("Describe only works on structs"),
 };

 // Generate the impl
 quote! {
     impl Describe for #name {
         fn describe(&self) -> String {
             let mut parts = vec![format!("{}", stringify!(#name))];
             #( parts.push(format!("{}: {:?}", stringify!(#fields), self.#fields)); )*
             parts.join(", ")
         }
     }
 }.into()
}

// Usage in another crate:
#[derive(Describe)]
struct User { name: String, age: u32 }

let u = User { name: "Alice".into(), age: 30 };
println!("{}", u.describe());  // "User, name: "Alice", age: 30"
1. `parse_macro_input!(input as DeriveInput)` โ€” parse tokens into a structured AST. 2. Navigate `data.fields` to find field names and types. 3. `quote! { impl Trait for #name { ... } }` โ€” generate the implementation. 4. `#( ... )*` in `quote!` iterates over a collection, like `macro_rules!` repetition.

What This Unlocks

Key Differences

ConceptOCamlRust
Auto-derived implementations`[@@deriving show, eq]` via `ppx_deriving``#[derive(Trait)]` via proc macro
Field introspection`Ppxlib` AST traversal`syn::DeriveInput` โ†’ `Data::Struct` โ†’ `Fields`
Code generation`Ppxlib.Ast_builder.Default``quote!` macro with `#name`, `#(...)* `
Separate cratePPX is separate libraryproc-macro crate required
Attribute reading`[@attr]` on fields`field.attrs` in `syn` โ€” parse with `syn::parse`
// Custom #[derive(MyTrait)] โ€” concept and simulation

// ===========================================================
// REAL PROC MACRO (separate crate, shown for reference):
//
// #[proc_macro_derive(Builder, attributes(builder))]
// pub fn derive_builder(input: TokenStream) -> TokenStream {
//     let input = parse_macro_input!(input as DeriveInput);
//     let name = &input.ident;
//     let builder_name = Ident::new(&format!("{}Builder", name), name.span());
//
//     let fields = match &input.data {
//         Data::Struct(DataStruct { fields: Fields::Named(f), .. }) => &f.named,
//         _ => panic!("Builder only supports named structs"),
//     };
//
//     let field_names: Vec<_> = fields.iter().map(|f| &f.ident).collect();
//     let field_types: Vec<_> = fields.iter().map(|f| &f.ty).collect();
//
//     quote! {
//         struct #builder_name {
//             #( #field_names: Option<#field_types> ),*
//         }
//         impl #builder_name {
//             #( pub fn #field_names(mut self, v: #field_types) -> Self {
//                 self.#field_names = Some(v); self
//             } )*
//             pub fn build(self) -> Result<#name, String> {
//                 Ok(#name {
//                     #( #field_names: self.#field_names.ok_or(stringify!(#field_names))? ),*
//                 })
//             }
//         }
//     }
// }
// ===========================================================

// Simulation: macro_rules! mimics what the proc macro would generate

macro_rules! derive_builder_sim {
    (
        struct $name:ident {
            $($field:ident : $ty:ty),* $(,)?
        }
    ) => {
        #[derive(Debug)]
        struct $name { $($field: $ty,)* }

        paste::paste! {
            struct [<$name Builder>] { $($field: Option<$ty>,)* }
        }
    };
}

// Manual simulation of proc macro output for DatabaseConfig
#[derive(Debug, Clone)]
struct DatabaseConfig {
    host: String,
    port: u16,
    database: String,
    username: String,
    password: String,
    max_pool: u32,
}

// What #[derive(Builder)] would generate:
struct DatabaseConfigBuilder {
    host: Option<String>,
    port: Option<u16>,
    database: Option<String>,
    username: Option<String>,
    password: Option<String>,
    max_pool: Option<u32>,
}

impl DatabaseConfigBuilder {
    fn new() -> Self {
        DatabaseConfigBuilder {
            host: None, port: None, database: None,
            username: None, password: None, max_pool: None,
        }
    }
    fn host(mut self, v: impl Into<String>) -> Self { self.host = Some(v.into()); self }
    fn port(mut self, v: u16) -> Self { self.port = Some(v); self }
    fn database(mut self, v: impl Into<String>) -> Self { self.database = Some(v.into()); self }
    fn username(mut self, v: impl Into<String>) -> Self { self.username = Some(v.into()); self }
    fn password(mut self, v: impl Into<String>) -> Self { self.password = Some(v.into()); self }
    fn max_pool(mut self, v: u32) -> Self { self.max_pool = Some(v); self }

    fn build(self) -> Result<DatabaseConfig, String> {
        Ok(DatabaseConfig {
            host: self.host.ok_or("host is required")?,
            port: self.port.unwrap_or(5432),
            database: self.database.ok_or("database is required")?,
            username: self.username.ok_or("username is required")?,
            password: self.password.unwrap_or_default(),
            max_pool: self.max_pool.unwrap_or(10),
        })
    }
}

impl DatabaseConfig {
    fn builder() -> DatabaseConfigBuilder { DatabaseConfigBuilder::new() }
}

fn main() {
    let config = DatabaseConfig::builder()
        .host("localhost")
        .database("myapp")
        .username("admin")
        .password("secret")
        .max_pool(20)
        .build()
        .unwrap();

    println!("{:?}", config);
    println!("{}:{}/{}", config.host, config.port, config.database);
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_builder() {
        let c = DatabaseConfig::builder()
            .host("db.example.com")
            .database("prod")
            .username("app")
            .build()
            .unwrap();
        assert_eq!(c.host, "db.example.com");
        assert_eq!(c.port, 5432); // default
    }

    #[test]
    fn test_builder_missing_required() {
        let r = DatabaseConfig::builder().build();
        assert!(r.is_err());
    }
}
(* Custom derive concept in OCaml *)

(* ppx_deriving.show generates to_string/pp functions *)
(* We simulate what it generates manually *)

type color = Red | Green | Blue | Custom of int * int * int

(* Generated by [@@deriving show] *)
let pp_color fmt = function
  | Red -> Format.pp_print_string fmt "Red"
  | Green -> Format.pp_print_string fmt "Green"
  | Blue -> Format.pp_print_string fmt "Blue"
  | Custom (r,g,b) -> Format.fprintf fmt "Custom (%d, %d, %d)" r g b

let show_color c =
  let buf = Buffer.create 16 in
  let fmt = Format.formatter_of_buffer buf in
  pp_color fmt c;
  Format.pp_print_flush fmt ();
  Buffer.contents buf

(* Generated by [@@deriving eq] *)
let equal_color a b = match (a, b) with
  | (Red, Red) | (Green, Green) | (Blue, Blue) -> true
  | (Custom (r1,g1,b1), Custom (r2,g2,b2)) -> r1=r2 && g1=g2 && b1=b2
  | _ -> false

let () =
  let colors = [Red; Green; Blue; Custom (128, 64, 255)] in
  List.iter (fun c -> Printf.printf "%s\n" (show_color c)) colors;
  Printf.printf "Red = Red: %b\n" (equal_color Red Red);
  Printf.printf "Red = Blue: %b\n" (equal_color Red Blue)