395: Default Methods
Difficulty: 2 Level: Intermediate Provide method implementations in a trait so implementors get them free โ override only what needs customization.The Problem This Solves
Traits are interfaces: they declare what a type can do, but normally leave the "how" entirely to the implementor. Every type must implement every method. This works until your trait grows. Add ten utility methods โ `is_empty`, `len`, `contains`, `any`, `all`, `first`, `last` โ and every implementor must write them all, usually with the same logic. This is the interface tax. In Java, it led to `AbstractXxx` base classes โ ugly workarounds to share default behavior. In Rust, default methods solve it cleanly: you provide an implementation in the trait definition. Implementors that are happy with the default skip it. Those with better implementations for their specific type can override it. The minimal surface is small; the full API is rich. The standard library uses this heavily. `Iterator` requires only `next()` โ then provides 70+ default methods. `Read` requires only `read()` then provides `read_exact`, `read_to_end`, `bytes`, and more. One method to implement; a full API to use.The Intuition
Default methods are fallback implementations living in the trait itself. They're written in terms of the required (abstract) methods โ anything the implementor must provide. When you call a default method, it delegates to the required methods, which dispatch to the concrete type's implementation. It's like a mixin: define the core operations abstractly, then build higher-level operations on top. The higher-level operations work correctly for any type that properly implements the core.How It Works in Rust
use std::fmt;
trait Collection {
type Item: PartialEq + fmt::Debug + Clone;
// Required: implementors MUST provide this
fn items(&self) -> &[Self::Item];
// Default methods: free for all implementors
fn is_empty(&self) -> bool { self.items().is_empty() }
fn len(&self) -> usize { self.items().len() }
fn contains(&self, item: &Self::Item) -> bool { self.items().contains(item) }
fn any(&self, predicate: impl Fn(&Self::Item) -> bool) -> bool {
self.items().iter().any(predicate)
}
fn all(&self, predicate: impl Fn(&Self::Item) -> bool) -> bool {
self.items().iter().all(predicate)
}
fn first(&self) -> Option<&Self::Item> { self.items().first() }
}
struct IntVec(Vec<i32>);
// Minimal impl: just the one required method
impl Collection for IntVec {
type Item = i32;
fn items(&self) -> &[i32] { &self.0 }
// is_empty, len, contains, any, all, first โ all come for free
}
fn main() {
let v = IntVec(vec![1, 2, 3, 4, 5]);
println!("len: {}", v.len()); // default method
println!("contains 3: {}", v.contains(&3)); // default method
println!("any > 4: {}", v.any(|x| *x > 4)); // default method
}
Override a default when you have a more efficient implementation:
struct SortedVec(Vec<i32>);
impl Collection for SortedVec {
type Item = i32;
fn items(&self) -> &[i32] { &self.0 }
// Binary search is O(log n) vs default's O(n) linear scan
fn contains(&self, item: &i32) -> bool {
self.0.binary_search(item).is_ok()
}
}
What This Unlocks
- Implement once, use everywhere โ `Iterator::next()` is all you write; `.map()`, `.filter()`, `.fold()`, `.zip()`, and 65 others come for free.
- API evolution without breaking changes โ add new default methods to a published trait; existing implementors continue to compile and automatically gain the new behavior.
- Focused required surface โ types only need to implement the core semantic operation; boilerplate utilities are inherited.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Default methods | Functor `MakeCollection` fills in defaults from base operations | Default methods directly in `trait` body โ no extra module indirection |
| Required vs default | Functor parameter must supply all base operations | Methods without `{}` body are required; those with `{}` are optional to override |
| Override | Functor always uses provided base โ no override | Implementor can shadow any default with a more efficient version |
| Tooling | Each functor application creates a new module | Single trait; auto-complete shows all methods as available |
// Default method implementations in Rust
use std::fmt;
trait Collection {
type Item: PartialEq + fmt::Debug + Clone;
// Core methods โ must implement
fn items(&self) -> &[Self::Item];
// Default methods โ free from core
fn is_empty(&self) -> bool {
self.items().is_empty()
}
fn len(&self) -> usize {
self.items().len()
}
fn contains(&self, item: &Self::Item) -> bool {
self.items().contains(item)
}
fn count_if(&self, predicate: impl Fn(&Self::Item) -> bool) -> usize {
self.items().iter().filter(|x| predicate(x)).count()
}
fn any(&self, predicate: impl Fn(&Self::Item) -> bool) -> bool {
self.items().iter().any(predicate)
}
fn all(&self, predicate: impl Fn(&Self::Item) -> bool) -> bool {
self.items().iter().all(predicate)
}
fn first(&self) -> Option<&Self::Item> {
self.items().first()
}
fn last(&self) -> Option<&Self::Item> {
self.items().last()
}
fn to_vec(&self) -> Vec<Self::Item> {
self.items().to_vec()
}
fn describe(&self) -> String where Self::Item: fmt::Display {
let s: Vec<String> = self.items().iter().map(|x| x.to_string()).collect();
format!("[{}]", s.join(", "))
}
}
struct IntVec(Vec<i32>);
struct StrVec(Vec<String>);
impl Collection for IntVec {
type Item = i32;
fn items(&self) -> &[i32] { &self.0 }
}
impl Collection for StrVec {
type Item = String;
fn items(&self) -> &[String] { &self.0 }
// Override just one default:
fn is_empty(&self) -> bool { self.0.is_empty() }
}
fn main() {
let v = IntVec(vec![1, 2, 3, 4, 5]);
println!("is_empty: {}", v.is_empty());
println!("len: {}", v.len());
println!("contains 3: {}", v.contains(&3));
println!("any > 4: {}", v.any(|x| *x > 4));
println!("all > 0: {}", v.all(|x| *x > 0));
println!("count > 2: {}", v.count_if(|x| *x > 2));
println!("describe: {}", v.describe());
let sv = StrVec(vec!["hello".into(), "world".into()]);
println!("StrVec contains 'hello': {}", sv.contains(&"hello".to_string()));
println!("StrVec first: {:?}", sv.first());
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_defaults() {
let v = IntVec(vec![10, 20, 30]);
assert_eq!(v.len(), 3);
assert!(!v.is_empty());
assert!(v.contains(&20));
assert!(v.any(|x| *x > 25));
assert!(!v.all(|x| *x > 15));
}
}
(* Default methods via OCaml functor with defaults *)
module type Collection = sig
type t
type item
val is_empty : t -> bool
val size : t -> int
val to_list : t -> item list
(* "Default" methods derived from the above *)
val contains : item -> t -> bool
val count_if : (item -> bool) -> t -> int
val any : (item -> bool) -> t -> bool
val all : (item -> bool) -> t -> bool
end
module MakeCollection (Base : sig
type t
type item
val is_empty : t -> bool
val size : t -> int
val to_list : t -> item list
val equal : item -> item -> bool
end) : Collection with type t = Base.t and type item = Base.item = struct
include Base
let contains x c = List.exists (Base.equal x) (Base.to_list c)
let count_if f c = List.length (List.filter f (Base.to_list c))
let any f c = List.exists f (Base.to_list c)
let all f c = List.for_all f (Base.to_list c)
end
module IntVec = MakeCollection(struct
type t = int list
type item = int
let is_empty = function [] -> true | _ -> false
let size = List.length
let to_list x = x
let equal = (=)
end)
let () =
let v = [1;2;3;4;5] in
Printf.printf "is_empty: %b\n" (IntVec.is_empty v);
Printf.printf "size: %d\n" (IntVec.size v);
Printf.printf "contains 3: %b\n" (IntVec.contains 3 v);
Printf.printf "any >4: %b\n" (IntVec.any (fun x -> x > 4) v);
Printf.printf "all >0: %b\n" (IntVec.all (fun x -> x > 0) v)