๐Ÿฆ€ Functional Rust

407: Default Trait and Initialization

Difficulty: 1 Level: Beginner The `Default` trait provides a standard way to create "zero value" instances of your types.

The Problem This Solves

Every type needs a sensible starting state. Counters start at zero. Strings start empty. Config structs have sane defaults (host: "localhost", port: 8080, retries: 3). Without a standard way to express this, every API that needs a default must either hardcode it internally or require the caller to provide all fields. `Default` solves this by giving every type a canonical zero value. The standard library uses it everywhere: `Option::unwrap_or_default()`, `HashMap::entry().or_default()`, struct update syntax `Config { port: 9090, ..Config::default() }`. Without `Default`, all of these APIs would need separate workarounds. For simple types, `#[derive(Default)]` works automatically: numbers get 0, booleans get false, strings get empty, Options get None, Vecs get empty. For types with domain-specific sensible defaults, you implement `Default` manually and set meaningful values.

The Intuition

`Default` is a trait with a single method: `fn default() -> Self`. That's it. Implementing it says "this type has a canonical empty/zero/initial state." The `#[derive(Default)]` macro generates this automatically by calling `Default::default()` on each field โ€” so every field type must also implement `Default`. The real value is composability: once your type implements `Default`, the entire standard library ecosystem that uses `Default` bounds works with your type automatically.

How It Works in Rust

// Derive: fields get language-level zero values
#[derive(Debug, Default)]
struct ServerConfig {
 host: String,        // ""
 port: u16,           // 0
 max_connections: u32, // 0
 debug: bool,         // false
}

// Manual: domain-specific sensible defaults
#[derive(Debug)]
struct AppConfig {
 host: String,
 port: u16,
 max_connections: u32,
 debug: bool,
 timeout_secs: f64,
 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,
     }
 }
}

fn main() {
 // Struct update syntax: override only what changes
 let custom = AppConfig {
     port: 9090,
     debug: true,
     ..AppConfig::default()  // fill remaining fields from default
 };
 println!("{:?}", custom);

 // or_default() in collections โ€” elegant, no unwrap needed
 use std::collections::HashMap;
 let mut word_count: HashMap<&str, u32> = HashMap::new();
 for word in ["hello", "world", "hello", "rust", "hello"] {
     *word_count.entry(word).or_default() += 1;
     // or_default() returns &mut u32, creating 0 if key absent
 }
 println!("{:?}", word_count); // {"hello": 3, "world": 1, "rust": 1}

 // unwrap_or_default: None becomes the type's default
 let opt: Option<Vec<i32>> = None;
 let v = opt.unwrap_or_default();
 println!("unwrap_or_default: {:?}", v); // []
}
A useful pattern โ€” generic builders that fill missing fields with defaults:
fn configure(overrides: impl FnOnce(&mut AppConfig)) -> AppConfig {
 let mut cfg = AppConfig::default();
 overrides(&mut cfg);
 cfg
}

let cfg = configure(|c| { c.port = 9000; c.debug = true; });

What This Unlocks

Key Differences

ConceptOCamlRust
Default value`default_config` record literal โ€” a value, not a trait`Default` trait โ€” standardized, works with generics and `#[derive]`
Struct update`{ default_config with port = 9090 }` โ€” same record-update syntax`Config { port: 9090, ..Config::default() }` โ€” identical idiom
Collection defaults`Hashtbl.find_opt` + manual `Option.value``entry().or_default()` โ€” single method, uses `Default`
PropagationManual in each module`#[derive(Default)]` propagates โ€” works if all fields are `Default`
// Default trait and initialization in Rust

#[derive(Debug, Default, Clone)]
struct ServerConfig {
    host: String,      // default: ""
    port: u16,         // default: 0
    max_connections: u32, // default: 0
    debug: bool,       // default: false
    timeout_secs: f64, // default: 0.0
}

// Custom Default for non-trivial defaults
#[derive(Debug, Clone)]
struct AppConfig {
    host: String,
    port: u16,
    max_connections: u32,
    debug: bool,
    timeout_secs: f64,
    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,
        }
    }
}

#[derive(Debug, Default)]
struct Counter { count: u64, total: u64 }

impl Counter {
    fn increment(&mut self) { self.count += 1; self.total += 1; }
    fn average(&self) -> f64 { if self.count == 0 { 0.0 } else { self.total as f64 / self.count as f64 } }
}

fn main() {
    // Derive Default
    let config = ServerConfig::default();
    println!("{:?}", config);

    // Struct update syntax with custom Default
    let custom = AppConfig {
        port: 9090,
        debug: true,
        ..AppConfig::default()  // fill the rest with defaults
    };
    println!("{:?}", custom);

    // Using Default in collections
    use std::collections::HashMap;
    let mut word_count: HashMap<&str, u32> = HashMap::new();
    let words = ["hello", "world", "hello", "rust", "world", "hello"];
    for word in &words {
        *word_count.entry(word).or_default() += 1; // or_default() uses u32::default() = 0
    }
    println!("{:?}", word_count);

    // Option::unwrap_or_default
    let opt: Option<Vec<i32>> = None;
    let v = opt.unwrap_or_default();
    println!("unwrap_or_default: {:?}", v); // []

    // Counter with Default
    let mut c = Counter::default();
    for _ in 0..5 { c.increment(); }
    println!("Count: {}, Average: {}", c.count, c.average());
}

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

    #[test]
    fn test_derive_default() {
        let c = ServerConfig::default();
        assert_eq!(c.host, "");
        assert_eq!(c.port, 0);
        assert!(!c.debug);
    }

    #[test]
    fn test_custom_default() {
        let c = AppConfig::default();
        assert_eq!(c.host, "localhost");
        assert_eq!(c.port, 8080);
        assert_eq!(c.retry_count, 3);
    }

    #[test]
    fn test_struct_update() {
        let c = AppConfig { port: 9000, ..AppConfig::default() };
        assert_eq!(c.port, 9000);
        assert_eq!(c.host, "localhost"); // from default
    }
}
(* Default trait concept in OCaml *)

(* Zero-value initialization *)
type server_config = {
  host: string;
  port: int;
  max_connections: int;
  debug: bool;
  timeout_seconds: float;
}

let default_config = {
  host = "localhost";
  port = 8080;
  max_connections = 100;
  debug = false;
  timeout_seconds = 30.0;
}

(* Struct update *)
let make_config ?host ?port ?debug () =
  { default_config with
    host = Option.value host ~default:default_config.host;
    port = Option.value port ~default:default_config.port;
    debug = Option.value debug ~default:default_config.debug;
  }

let print_config c =
  Printf.printf "Config { host=%s, port=%d, max=%d, debug=%b, timeout=%.1f }\n"
    c.host c.port c.max_connections c.debug c.timeout_seconds

let () =
  print_config default_config;
  let custom = make_config ~port:9090 ~debug:true () in
  print_config custom