๐Ÿฆ€ Functional Rust

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

Key Differences

ConceptOCamlRust
Default methodsFunctor `MakeCollection` fills in defaults from base operationsDefault methods directly in `trait` body โ€” no extra module indirection
Required vs defaultFunctor parameter must supply all base operationsMethods without `{}` body are required; those with `{}` are optional to override
OverrideFunctor always uses provided base โ€” no overrideImplementor can shadow any default with a more efficient version
ToolingEach functor application creates a new moduleSingle 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)