ExamplesBy LevelBy TopicLearning Paths
407 Fundamental

407: The Default Trait

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "407: The Default Trait" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Large configuration structs become painful to construct when callers must specify every field. Key difference from OCaml: 1. **Mechanism**: Rust uses a trait with a single `default()` method; OCaml uses optional function parameters — fundamentally different approaches.

Tutorial

The Problem

Large configuration structs become painful to construct when callers must specify every field. Languages with named parameters or optional fields handle this naturally, but Rust requires all fields to be specified in struct literals. The Default trait solves this: implementing Default for a struct lets callers use ..Default::default() to fill in unspecified fields, and the struct update syntax to customize only what differs from the default. This is the idiomatic Rust approach to optional constructor parameters.

Default appears everywhere: HashMap::new() uses Default::default() internally, Vec::new() returns an empty vec (the default), and derive macros require Default for many generated methods.

🎯 Learning Outcomes

  • • Understand the Default trait as the standard way to create "empty" or "starter" values
  • • Learn the difference between derived Default (zeros/empty) and custom Default implementations
  • • See the struct update syntax ..Default::default() for partial initialization
  • • Understand how Default enables the builder pattern and #[derive(Default)] on config types
  • • Learn which standard types implement Default and what they return
  • Code Example

    #[derive(Debug, Clone)]
    struct Config {
        host: String,
        port: u16,
        debug: bool,
    }
    
    impl Default for Config {
        fn default() -> Self {
            Config {
                host: "localhost".to_string(),
                port: 8080,
                debug: false,
            }
        }
    }
    
    // Struct update syntax
    let custom = Config {
        port: 9000,
        ..Default::default()
    };

    Key Differences

  • Mechanism: Rust uses a trait with a single default() method; OCaml uses optional function parameters — fundamentally different approaches.
  • Granularity: Rust's Default returns the entire struct; OCaml's optional params default each field independently at the call site.
  • Struct update: Rust's ..Default::default() copies all remaining fields; OCaml has no equivalent (you specify each field).
  • Derive: Rust's #[derive(Default)] works for any struct where all fields implement Default; OCaml's deriving requires a ppx extension.
  • OCaml Approach

    OCaml achieves default values through optional parameters with ~ and ? syntax: let make_config ?(host="localhost") ?(port=8080) () = { host; port }. This is more flexible than Rust's Default since each field can have an independent default without a special trait. OCaml's named optional arguments eliminate the need for a builder pattern or Default trait entirely in most cases.

    Full Source

    #![allow(clippy::all)]
    //! Default Trait
    //!
    //! Providing default values for types — derivable or custom.
    
    use std::collections::HashMap;
    
    /// Server configuration with derived Default (all zeros/empty).
    #[derive(Debug, Default, Clone, PartialEq)]
    pub struct ServerConfig {
        pub host: String,
        pub port: u16,
        pub max_connections: u32,
        pub debug: bool,
        pub timeout_secs: f64,
    }
    
    /// Application configuration with custom Default.
    #[derive(Debug, Clone, PartialEq)]
    pub struct AppConfig {
        pub host: String,
        pub port: u16,
        pub max_connections: u32,
        pub debug: bool,
        pub timeout_secs: f64,
        pub retry_count: u8,
    }
    
    impl Default for AppConfig {
        fn default() -> Self {
            AppConfig {
                host: "localhost".to_string(),
                port: 8080,
                max_connections: 100,
                debug: false,
                timeout_secs: 30.0,
                retry_count: 3,
            }
        }
    }
    
    impl AppConfig {
        /// Creates a new config with custom port.
        pub fn with_port(port: u16) -> Self {
            AppConfig {
                port,
                ..Default::default()
            }
        }
    
        /// Creates a debug-enabled config.
        pub fn debug() -> Self {
            AppConfig {
                debug: true,
                ..Default::default()
            }
        }
    }
    
    /// A simple counter with Default initialization.
    #[derive(Debug, Default, Clone)]
    pub struct Counter {
        pub count: u64,
        pub sum: u64,
    }
    
    impl Counter {
        pub fn new() -> Self {
            Counter::default()
        }
    
        pub fn increment(&mut self, value: u64) {
            self.count += 1;
            self.sum += value;
        }
    
        pub fn average(&self) -> f64 {
            if self.count == 0 {
                0.0
            } else {
                self.sum as f64 / self.count as f64
            }
        }
    }
    
    /// Counts word occurrences using or_default().
    pub fn count_words(words: &[&str]) -> HashMap<String, u32> {
        let mut counts = HashMap::new();
        for word in words {
            *counts.entry(word.to_string()).or_default() += 1;
        }
        counts
    }
    
    /// Gets value from Option or Default.
    pub fn get_or_default<T: Default>(opt: Option<T>) -> T {
        opt.unwrap_or_default()
    }
    
    /// Generic function requiring Default bound.
    pub fn default_if_empty<T: Default>(value: Option<T>) -> T {
        value.unwrap_or_default()
    }
    
    /// A builder pattern using Default.
    #[derive(Debug, Clone)]
    pub struct RequestBuilder {
        pub method: String,
        pub url: String,
        pub headers: HashMap<String, String>,
        pub timeout_ms: u64,
    }
    
    impl Default for RequestBuilder {
        fn default() -> Self {
            RequestBuilder {
                method: "GET".to_string(),
                url: String::new(),
                headers: HashMap::new(),
                timeout_ms: 5000,
            }
        }
    }
    
    impl RequestBuilder {
        pub fn new() -> Self {
            Default::default()
        }
    
        pub fn method(mut self, method: &str) -> Self {
            self.method = method.to_string();
            self
        }
    
        pub fn url(mut self, url: &str) -> Self {
            self.url = url.to_string();
            self
        }
    
        pub fn header(mut self, key: &str, value: &str) -> Self {
            self.headers.insert(key.to_string(), value.to_string());
            self
        }
    
        pub fn timeout(mut self, ms: u64) -> Self {
            self.timeout_ms = ms;
            self
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_server_config_default() {
            let cfg = ServerConfig::default();
            assert_eq!(cfg.host, "");
            assert_eq!(cfg.port, 0);
            assert_eq!(cfg.max_connections, 0);
            assert!(!cfg.debug);
            assert_eq!(cfg.timeout_secs, 0.0);
        }
    
        #[test]
        fn test_app_config_custom_default() {
            let cfg = AppConfig::default();
            assert_eq!(cfg.host, "localhost");
            assert_eq!(cfg.port, 8080);
            assert_eq!(cfg.max_connections, 100);
            assert!(!cfg.debug);
            assert_eq!(cfg.timeout_secs, 30.0);
            assert_eq!(cfg.retry_count, 3);
        }
    
        #[test]
        fn test_struct_update_syntax() {
            let cfg = AppConfig {
                port: 9000,
                debug: true,
                ..Default::default()
            };
            assert_eq!(cfg.port, 9000);
            assert!(cfg.debug);
            assert_eq!(cfg.host, "localhost"); // from default
        }
    
        #[test]
        fn test_counter_default() {
            let mut c = Counter::default();
            assert_eq!(c.count, 0);
            assert_eq!(c.sum, 0);
            c.increment(10);
            c.increment(20);
            assert_eq!(c.count, 2);
            assert_eq!(c.sum, 30);
            assert_eq!(c.average(), 15.0);
        }
    
        #[test]
        fn test_count_words() {
            let words = vec!["hello", "world", "hello", "rust"];
            let counts = count_words(&words);
            assert_eq!(counts.get("hello"), Some(&2));
            assert_eq!(counts.get("world"), Some(&1));
            assert_eq!(counts.get("rust"), Some(&1));
        }
    
        #[test]
        fn test_unwrap_or_default() {
            let some_vec: Option<Vec<i32>> = Some(vec![1, 2, 3]);
            let none_vec: Option<Vec<i32>> = None;
    
            assert_eq!(get_or_default(some_vec), vec![1, 2, 3]);
            assert_eq!(get_or_default(none_vec), Vec::<i32>::new());
        }
    
        #[test]
        fn test_request_builder() {
            let req = RequestBuilder::new()
                .method("POST")
                .url("https://api.example.com")
                .header("Content-Type", "application/json")
                .timeout(10000);
    
            assert_eq!(req.method, "POST");
            assert_eq!(req.url, "https://api.example.com");
            assert_eq!(
                req.headers.get("Content-Type"),
                Some(&"application/json".to_string())
            );
            assert_eq!(req.timeout_ms, 10000);
        }
    
        #[test]
        fn test_default_builder() {
            let req = RequestBuilder::default();
            assert_eq!(req.method, "GET");
            assert!(req.url.is_empty());
            assert!(req.headers.is_empty());
            assert_eq!(req.timeout_ms, 5000);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_server_config_default() {
            let cfg = ServerConfig::default();
            assert_eq!(cfg.host, "");
            assert_eq!(cfg.port, 0);
            assert_eq!(cfg.max_connections, 0);
            assert!(!cfg.debug);
            assert_eq!(cfg.timeout_secs, 0.0);
        }
    
        #[test]
        fn test_app_config_custom_default() {
            let cfg = AppConfig::default();
            assert_eq!(cfg.host, "localhost");
            assert_eq!(cfg.port, 8080);
            assert_eq!(cfg.max_connections, 100);
            assert!(!cfg.debug);
            assert_eq!(cfg.timeout_secs, 30.0);
            assert_eq!(cfg.retry_count, 3);
        }
    
        #[test]
        fn test_struct_update_syntax() {
            let cfg = AppConfig {
                port: 9000,
                debug: true,
                ..Default::default()
            };
            assert_eq!(cfg.port, 9000);
            assert!(cfg.debug);
            assert_eq!(cfg.host, "localhost"); // from default
        }
    
        #[test]
        fn test_counter_default() {
            let mut c = Counter::default();
            assert_eq!(c.count, 0);
            assert_eq!(c.sum, 0);
            c.increment(10);
            c.increment(20);
            assert_eq!(c.count, 2);
            assert_eq!(c.sum, 30);
            assert_eq!(c.average(), 15.0);
        }
    
        #[test]
        fn test_count_words() {
            let words = vec!["hello", "world", "hello", "rust"];
            let counts = count_words(&words);
            assert_eq!(counts.get("hello"), Some(&2));
            assert_eq!(counts.get("world"), Some(&1));
            assert_eq!(counts.get("rust"), Some(&1));
        }
    
        #[test]
        fn test_unwrap_or_default() {
            let some_vec: Option<Vec<i32>> = Some(vec![1, 2, 3]);
            let none_vec: Option<Vec<i32>> = None;
    
            assert_eq!(get_or_default(some_vec), vec![1, 2, 3]);
            assert_eq!(get_or_default(none_vec), Vec::<i32>::new());
        }
    
        #[test]
        fn test_request_builder() {
            let req = RequestBuilder::new()
                .method("POST")
                .url("https://api.example.com")
                .header("Content-Type", "application/json")
                .timeout(10000);
    
            assert_eq!(req.method, "POST");
            assert_eq!(req.url, "https://api.example.com");
            assert_eq!(
                req.headers.get("Content-Type"),
                Some(&"application/json".to_string())
            );
            assert_eq!(req.timeout_ms, 10000);
        }
    
        #[test]
        fn test_default_builder() {
            let req = RequestBuilder::default();
            assert_eq!(req.method, "GET");
            assert!(req.url.is_empty());
            assert!(req.headers.is_empty());
            assert_eq!(req.timeout_ms, 5000);
        }
    }

    Deep Comparison

    OCaml vs Rust: Default Trait

    Side-by-Side Code

    OCaml — Record default via let binding

    type config = {
      host: string;
      port: int;
      debug: bool;
    }
    
    let default_config = {
      host = "localhost";
      port = 8080;
      debug = false;
    }
    
    (* Struct update *)
    let custom = { default_config with port = 9000 }
    

    Rust — Default trait

    #[derive(Debug, Clone)]
    struct Config {
        host: String,
        port: u16,
        debug: bool,
    }
    
    impl Default for Config {
        fn default() -> Self {
            Config {
                host: "localhost".to_string(),
                port: 8080,
                debug: false,
            }
        }
    }
    
    // Struct update syntax
    let custom = Config {
        port: 9000,
        ..Default::default()
    };
    

    Comparison Table

    AspectOCamlRust
    Default valuesNamed let bindingDefault trait
    DerivableNo#[derive(Default)] for zero-values
    Struct update{ record with field = value }{ field, ..Default::default() }
    Collection defaultNot standardizedor_default(), unwrap_or_default()
    Generic boundN/AT: Default

    Derive vs Custom

    // Derive: all fields use their Default (0, false, "", etc.)
    #[derive(Default)]
    struct Point { x: i32, y: i32 }
    // Point::default() → Point { x: 0, y: 0 }
    
    // Custom: you choose the values
    impl Default for Config {
        fn default() -> Self {
            Config { host: "localhost".into(), port: 8080, debug: false }
        }
    }
    

    Common Patterns

    or_default() in Collections

    let mut counts: HashMap<&str, u32> = HashMap::new();
    *counts.entry("key").or_default() += 1;  // u32::default() = 0
    

    unwrap_or_default()

    let opt: Option<Vec<i32>> = None;
    let v = opt.unwrap_or_default();  // Vec::default() = []
    

    Generic Functions

    fn ensure<T: Default>(opt: Option<T>) -> T {
        opt.unwrap_or_default()
    }
    

    5 Takeaways

  • **#[derive(Default)] works for zero-like defaults.**
  • Numbers → 0, bools → false, strings → empty.

  • **Custom impl Default for meaningful defaults.**
  • Port 8080, timeout 30s, etc.

  • **Struct update syntax uses ..Default::default().**
  • Override specific fields, fill rest with defaults.

  • **or_default() is idiomatic for HashMap counters.**
  • No need for entry().or_insert(0).

  • OCaml uses named values; Rust uses a trait.
  • default_config vs Config::default().

    Exercises

  • HTTP request builder: Create HttpRequest { method: String, url: String, headers: Vec<(String, String)>, body: Option<Vec<u8>>, timeout: Duration } with a custom Default (GET, empty url, no headers, no body, 30s timeout). Show construction with ..Default::default().
  • Nested defaults: Create a nested config DatabaseConfig with Default and use it as a field in AppConfig. Show that AppConfig::default() initializes the nested struct correctly.
  • Default impl for custom types: Implement Default for a Matrix<f64> type that returns a 3x3 identity matrix. Explain in a comment why the identity matrix is the appropriate default for numeric matrix operations.
  • Open Source Repos