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
- Struct update syntax โ `MyStruct { field: value, ..MyStruct::default() }` lets callers specify only what's non-default; essential for large config structs.
- Collection ergonomics โ `entry().or_default()`, `unwrap_or_default()`, and `Option::get_or_insert_default()` eliminate boilerplate initialization patterns.
- Generic zero-value construction โ `fn reset<T: Default>(&mut self) { self.state = T::default(); }` works for any `Default` type without knowing the concrete type.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| 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` |
| Propagation | Manual 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