ExamplesBy LevelBy TopicLearning Paths
395 Intermediate

395: Default Methods in Traits

Functional Programming

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

  • • Understand how default methods reduce the implementation burden for trait consumers
  • • Learn how implementations can override defaults for customized behavior
  • • See how default methods can call other (potentially non-default) methods in the same trait
  • • Understand the design pattern: minimal required interface + maximal default interface
  • • Learn how Robot overrides greeting() while inheriting the formal_greeting() default
  • Code 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

  • 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-application.
  • Required vs. optional: Rust explicitly distinguishes required methods (no body) from default methods (with body); OCaml functors make all module members required in the parameter.
  • Self access: Rust default methods access self directly; OCaml class methods use self#method_name, functor defaults use the passed module value.
  • Stability: Adding a default method to a Rust trait is backward compatible; removing a default is breaking. OCaml functor additions are always breaking since they change the parameter signature.
  • 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"));
        }
    }
    ✓ Tests Rust test suite
    #[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

  • Iterator-like trait: Design a 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 defaults: Create a 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.
  • Override verification: Extend 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.
  • Open Source Repos