395: Default Methods in Traits
Tutorial Video
Text description (accessibility)
This video demonstrates the "395: Default Methods in Traits" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Trait evolution is a challenge: adding a new required method to a published trait breaks all existing implementors. Key difference from OCaml: 1. **Override mechanism**: Rust defaults are overridden by providing the method in the `impl` block; OCaml class defaults are overridden with `method! greeting = ...` or functor re
Tutorial
The Problem
Trait evolution is a challenge: adding a new required method to a published trait breaks all existing implementors. Default methods (introduced in Rust to solve this, analogous to Java 8's default interface methods) allow traits to provide method implementations that implementors can use or override. This enables adding new functionality to traits without breaking the ecosystem and reduces the "implement 20 methods just to satisfy a trait" problem by providing sensible defaults for derived functionality.
Default methods appear in Iterator (where only next is required but 70+ adapter methods have defaults), Read/Write in std::io, and virtually every non-trivial trait in std.
🎯 Learning Outcomes
Robot overrides greeting() while inheriting the formal_greeting() defaultCode Example
#![allow(clippy::all)]
//! Default Methods in Traits
pub trait Greeter {
fn name(&self) -> &str;
fn greeting(&self) -> String {
format!("Hello, {}!", self.name())
}
fn formal_greeting(&self) -> String {
format!("Dear {}", self.name())
}
}
pub struct Person {
pub name: String,
}
pub struct Robot {
pub id: u32,
}
impl Greeter for Person {
fn name(&self) -> &str {
&self.name
}
}
impl Greeter for Robot {
fn name(&self) -> &str {
"Robot"
}
fn greeting(&self) -> String {
format!("BEEP BOOP - Unit {}", self.id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_person_greeting() {
let p = Person {
name: "Alice".into(),
};
assert!(p.greeting().contains("Alice"));
}
#[test]
fn test_person_formal() {
let p = Person { name: "Bob".into() };
assert!(p.formal_greeting().contains("Dear"));
}
#[test]
fn test_robot_override() {
let r = Robot { id: 42 };
assert!(r.greeting().contains("BEEP"));
}
#[test]
fn test_robot_default() {
let r = Robot { id: 1 };
assert!(r.formal_greeting().contains("Robot"));
}
}Key Differences
impl block; OCaml class defaults are overridden with method! greeting = ... or functor re-application.self directly; OCaml class methods use self#method_name, functor defaults use the passed module value.OCaml Approach
OCaml achieves default methods through module functors: module MakeGreeter (T : sig type t val name : t -> string end) = struct let greeting t = "Hello, " ^ T.name t ^ "!" end. Implementors apply the functor to get the defaults. OCaml's class methods can also provide defaults via method greeting = Printf.sprintf "Hello, %s!" self#name. Both approaches parallel Rust's defaults.
Full Source
#![allow(clippy::all)]
//! Default Methods in Traits
pub trait Greeter {
fn name(&self) -> &str;
fn greeting(&self) -> String {
format!("Hello, {}!", self.name())
}
fn formal_greeting(&self) -> String {
format!("Dear {}", self.name())
}
}
pub struct Person {
pub name: String,
}
pub struct Robot {
pub id: u32,
}
impl Greeter for Person {
fn name(&self) -> &str {
&self.name
}
}
impl Greeter for Robot {
fn name(&self) -> &str {
"Robot"
}
fn greeting(&self) -> String {
format!("BEEP BOOP - Unit {}", self.id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_person_greeting() {
let p = Person {
name: "Alice".into(),
};
assert!(p.greeting().contains("Alice"));
}
#[test]
fn test_person_formal() {
let p = Person { name: "Bob".into() };
assert!(p.formal_greeting().contains("Dear"));
}
#[test]
fn test_robot_override() {
let r = Robot { id: 42 };
assert!(r.greeting().contains("BEEP"));
}
#[test]
fn test_robot_default() {
let r = Robot { id: 1 };
assert!(r.formal_greeting().contains("Robot"));
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_person_greeting() {
let p = Person {
name: "Alice".into(),
};
assert!(p.greeting().contains("Alice"));
}
#[test]
fn test_person_formal() {
let p = Person { name: "Bob".into() };
assert!(p.formal_greeting().contains("Dear"));
}
#[test]
fn test_robot_override() {
let r = Robot { id: 42 };
assert!(r.greeting().contains("BEEP"));
}
#[test]
fn test_robot_default() {
let r = Robot { id: 1 };
assert!(r.formal_greeting().contains("Robot"));
}
}
Deep Comparison
OCaml vs Rust: 395-default-methods
Exercises
Stream trait with a required fn next(&mut self) -> Option<i32> and default methods fn collect_all(&mut self) -> Vec<i32>, fn take_n(&mut self, n: usize) -> Vec<i32>, and fn sum_all(&mut self) -> i32. Implement for a RangeStream and a FibStream.Builder trait with required fn name(&self) -> &str and defaults fn build_json(&self) -> String (JSON template) and fn build_toml(&self) -> String (TOML template). Two different structs should use the same defaults.Greeter with a fn loud_greeting(&self) -> String default that calls self.greeting().to_uppercase(). Verify that when Robot overrides greeting, the loud_greeting default automatically uses the overridden version.