407: The Default Trait
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
Default trait as the standard way to create "empty" or "starter" valuesDefault (zeros/empty) and custom Default implementations..Default::default() for partial initializationDefault enables the builder pattern and #[derive(Default)] on config typesDefault and what they returnCode 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
default() method; OCaml uses optional function parameters — fundamentally different approaches.Default returns the entire struct; OCaml's optional params default each field independently at the call site...Default::default() copies all remaining fields; OCaml has no equivalent (you specify each field).#[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);
}
}#[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
| Aspect | OCaml | Rust |
|---|---|---|
| Default values | Named let binding | Default trait |
| Derivable | No | #[derive(Default)] for zero-values |
| Struct update | { record with field = value } | { field, ..Default::default() } |
| Collection default | Not standardized | or_default(), unwrap_or_default() |
| Generic bound | N/A | T: 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.
impl Default for meaningful defaults.**Port 8080, timeout 30s, etc.
..Default::default().**Override specific fields, fill rest with defaults.
or_default() is idiomatic for HashMap counters.** No need for entry().or_insert(0).
default_config vs Config::default().
Exercises
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().DatabaseConfig with Default and use it as a field in AppConfig. Show that AppConfig::default() initializes the nested struct correctly.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.