Files
rodin-bot a79249bbff fix: correct drifted citations at commit f53b654
- traits.md: iterator.rs anchor L76→L42 (pub const trait Iterator)
- traits.md: deref.rs anchor L60→L139 (pub const trait Deref)
- error-handling.md: fix variant names CliError::Io→IoError, Parse→ParseError
  to match actual stdlib doc example at convert/mod.rs:557
- concurrency.md: mutex.rs and rwlock.rs moved to sync/poison/ subtree
  (3 link updates: mutex×2, rwlock×1)
2026-05-06 17:22:48 -07:00

19 KiB

Rust Trait Patterns

Patterns for trait design in Rust, extracted from the standard library source at rust-lang/rust.

Source: rust-lang/rust at commit f53b654

Stats: 241 public traits, 4,387 trait implementations, 508 From impls, 895 Iterator impls, 269 Display impls, 226 Default impls.


1. Small Traits (One Required Method)

Source:

library/core/src/iter/traits/iterator.rs#L42

// library/core/src/iter/traits/iterator.rs
pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // 70+ provided methods built on top of `next()`...
    fn map<B, F>(self, f: F) -> Map<Self, F> { ... }
    fn filter<P>(self, predicate: P) -> Filter<Self, P> { ... }
    fn collect<B>(self) -> B where B: FromIterator<Self::Item> { ... }
    // ...
}

Iterator has 1 required method (next) and 70+ provided methods. Implementing Iterator for your type requires writing ONE function.

Why

Minimize the cost of implementing the trait while maximizing the value. One required method means trivial to implement; provided methods give the full API for free. 895 Iterator implementations exist in the stdlib because it's so easy.

When to Use

Triggers:

  • You want many types to implement your trait
  • There's a clear "minimal kernel" that all implementations share
  • Other methods can be defined in terms of that kernel

Example — before:

// Fat trait — every implementor must write 5 methods
trait Sequence {
    fn next(&mut self) -> Option<Self::Item>;
    fn size_hint(&self) -> (usize, Option<usize>);
    fn count(self) -> usize;
    fn last(self) -> Option<Self::Item>;
    fn nth(&mut self, n: usize) -> Option<Self::Item>;
}
// Result: few types implement it because it's too much work

Example — after:

// Minimal required, rest provided
trait Sequence {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;  // ONE required method

    // Provided defaults — free for all implementors:
    fn size_hint(&self) -> (usize, Option<usize>) { (0, None) }
    fn count(self) -> usize { self.fold(0, |c, _| c + 1) }
    fn last(self) -> Option<Self::Item> { self.fold(None, |_, x| Some(x)) }
    fn nth(&mut self, n: usize) -> Option<Self::Item> {
        for _ in 0..n { self.next(); }
        self.next()
    }
}

When NOT to Use

Don't use this when:

  • Methods are genuinely independent (can't be derived from each other)
  • Implementors SHOULD be forced to think about each method (e.g., PartialOrd requires partial_cmp because you can't derive comparison from equality)
  • The "default" would be wrong for most types

2. Derive Macros for Standard Traits

Source:

1,116 #[derive(...)] annotations in library/. Top derives: Clone (880), Debug (869), Copy (537), PartialEq (388), Eq (285).

// Typical stdlib type:
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Duration {
    secs: u64,
    nanos: Nanoseconds,
}

Why

Derive generates correct implementations mechanically. Hand-writing Clone/Debug/PartialEq is error-prone and repetitive. The derive order is a convention: Debug first, then Clone/Copy, then comparison traits, then Hash.

When to Use

Triggers:

  • Your struct/enum fields all implement the trait you want to derive
  • The derived behavior is correct (field-by-field comparison, etc.)
  • You want standard trait behavior without custom logic

Example — before:

struct Point { x: f64, y: f64 }

impl fmt::Debug for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Point")
            .field("x", &self.x)
            .field("y", &self.y)
            .finish()
    }
}
impl Clone for Point { ... }
impl PartialEq for Point { ... }
// 30 lines of boilerplate for standard behavior

Example — after:

#[derive(Debug, Clone, PartialEq)]
struct Point { x: f64, y: f64 }
// One line. Same result.

When NOT to Use

Don't use this when:

  • You need custom comparison logic (e.g., ignore certain fields)
  • Your type contains types that don't implement the trait
  • Derived behavior would be wrong (e.g., floating point and Eq)

Anti-pattern

// DON'T: Derive Eq on floats
#[derive(PartialEq, Eq)]  // COMPILE ERROR: f64 doesn't impl Eq
struct Measurement { value: f64 }

// DON'T: Derive PartialEq when it would be semantically wrong
#[derive(PartialEq)]
struct Handle { id: u64, cached_name: String }
// Do you really want cached_name to affect equality?

3. From/Into for Type Conversions

Source:

library/core/src/convert/mod.rs#L460

508 From<> implementations in the stdlib.

// Blanket impl: implementing From gives you Into for free
impl<T, U> Into<U> for T where U: From<T> {
    fn into(self) -> U { U::from(self) }
}

Why

From/Into is the standard conversion mechanism. Always implement From (not Into) because you get Into for free via the blanket impl. The stdlib says: "This trait must not fail. If the conversion can fail, use TryFrom."

When to Use

Triggers:

  • Conversion is infallible and lossless
  • Converting between wrapper types (newtype → inner, inner → newtype)
  • Error type conversion (see error handling patterns)

Example — before:

// Ad-hoc conversion methods with inconsistent names
impl Celsius {
    fn to_fahrenheit(&self) -> Fahrenheit { ... }
}
impl Fahrenheit {
    fn from_celsius(c: &Celsius) -> Fahrenheit { ... }
}
// Users must remember: is it .to_X() or ::from_X()?

Example — after:

impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}
// Now works with: Fahrenheit::from(c), c.into(), and generic bounds

When NOT to Use

Don't use this when:

  • Conversion can fail (use TryFrom)
  • Conversion is lossy (u32 → u16 can truncate)
  • It's an expensive operation that shouldn't happen implicitly

4. Default for Sensible Zero Values

Source:

library/core/src/default.rs

226 Default implementations in the stdlib.

pub trait Default: Sized {
    fn default() -> Self;
}

Why

Provides a canonical "empty" or "zero" value. Used by ..Default::default() struct update syntax, Option::unwrap_or_default(), collection initialization, and generic code that needs to create values.

When to Use

Triggers:

  • Your type has an obvious "empty" state (empty string, zero, nil)
  • You want ..Default::default() to work for partial struct init
  • Your type is used in generics bounded by Default

Example — before:

let config = Config {
    timeout: Duration::from_secs(30),
    retries: 3,
    verbose: false,
    port: 8080,
    host: String::new(),
};
// Must specify EVERY field even for "default" values

Example — after:

#[derive(Default)]
struct Config {
    timeout: Duration,      // Default: 0s (may need custom impl)
    retries: u32,           // Default: 0
    verbose: bool,          // Default: false
    port: u16,              // Default: 0
    host: String,           // Default: ""
}

let config = Config {
    port: 8080,
    retries: 3,
    ..Default::default()  // everything else gets defaults
};

When NOT to Use

Don't use this when:

  • There's no meaningful default (what's a "default" database URL?)
  • The zero value would be dangerous (default timeout of 0 = infinite)
  • You WANT users to think about every field

5. Display for Human-Readable Output

Source:

library/core/src/fmt/mod.rs#L1187

269 Display implementations in the stdlib.

pub trait Display: PointeeSized {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}

Why

Display is for user-facing output (what println!("{}", x) uses). Debug is for developer-facing output (println!("{:?}", x)). Every public type should implement both. Error trait REQUIRES Display.

When to Use

Triggers:

  • Your type will be printed/logged for end users
  • Your type implements Error (Display is required)
  • You want format!("{}", value) to work
  • The output should be human-readable (not debug-formatted)

Example — before:

// Only Debug — ugly output for users
#[derive(Debug)]
struct Version { major: u32, minor: u32, patch: u32 }

println!("{:?}", v);  // "Version { major: 1, minor: 2, patch: 3 }" — ugly

Example — after:

#[derive(Debug)]
struct Version { major: u32, minor: u32, patch: u32 }

impl fmt::Display for Version {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
    }
}

println!("{}", v);  // "1.2.3" — clean

When NOT to Use

Don't use this when:

  • There's no single canonical human representation
  • The type is internal and never shown to users
  • Debug output is sufficient (use #[derive(Debug)])

6. Iterator Implementation

Source:

library/core/src/iter/traits/iterator.rs

895 Iterator implementations in the stdlib.

Why

Implementing Iterator gives you the entire iterator ecosystem for free: .map(), .filter(), .collect(), for loops, and composability with every other iterator adapter.

When to Use

Triggers:

  • Your type represents a sequence of values
  • Users should be able to loop over it with for
  • You want composability with the iterator ecosystem

Example — before:

struct Fibonacci { a: u64, b: u64 }

impl Fibonacci {
    fn next_value(&mut self) -> u64 {
        let result = self.a;
        let new_b = self.a + self.b;
        self.a = self.b;
        self.b = new_b;
        result
    }
}
// Can't use with for loops, map, filter, take, collect...

Example — after:

struct Fibonacci { a: u64, b: u64 }

impl Iterator for Fibonacci {
    type Item = u64;
    fn next(&mut self) -> Option<Self::Item> {
        let result = self.a;
        let new_b = self.a.checked_add(self.b)?;  // overflow → end
        self.a = self.b;
        self.b = new_b;
        Some(result)
    }
}

// Now works with everything:
let first_10: Vec<u64> = Fibonacci { a: 0, b: 1 }.take(10).collect();

When NOT to Use

Don't use this when:

  • Your type doesn't represent a sequence
  • Access is random (implement Index instead)
  • The "iteration" has side effects that shouldn't compose

7. Deref for Smart Pointer Transparency

Source:

library/core/src/ops/deref.rs#L139

pub trait Deref {
    type Target: ?Sized;
    fn deref(&self) -> &Self::Target;
}

Why

Deref coercion makes smart pointers transparent: Box<T> behaves like T, String behaves like str, Vec<T> behaves like [T]. The compiler inserts .deref() calls implicitly.

When to Use

Triggers:

  • Your type is a smart pointer or wrapper around a single inner value
  • You want method calls to "pass through" to the inner type
  • There's a clear "target" type that your wrapper contains

Example — before:

struct MyString(Vec<u8>);

impl MyString {
    fn as_str(&self) -> &str {
        std::str::from_utf8(&self.0).unwrap()
    }
    fn len(&self) -> usize { self.as_str().len() }
    fn is_empty(&self) -> bool { self.as_str().is_empty() }
    fn contains(&self, pat: &str) -> bool { self.as_str().contains(pat) }
    // Must manually delegate EVERY str method...
}

Example — after:

struct MyString(Vec<u8>);

impl Deref for MyString {
    type Target = str;
    fn deref(&self) -> &str {
        std::str::from_utf8(&self.0).unwrap()
    }
}
// Now ALL str methods work automatically:
// my_string.len(), my_string.contains("x"), my_string.to_uppercase()

When NOT to Use

Don't use this when:

  • Your type is not semantically a wrapper/pointer
  • The deref target is surprising to users
  • You're using Deref as inheritance (Rust doesn't have inheritance — this abuse leads to confusing APIs)

Anti-pattern

// DON'T: Use Deref as "inheritance"
struct Dog { name: String }
struct GuideDog { dog: Dog, handler: String }

impl Deref for GuideDog {
    type Target = Dog;
    fn deref(&self) -> &Dog { &self.dog }
}
// This makes GuideDog "look like" Dog — misleading.
// Use composition + explicit delegation instead.

8. Sealed Traits (Restrict Implementors)

Source:

library/portable-simd/crates/core_simd/src/cast.rs

mod sealed {
    pub trait Sealed {}
}
pub trait SimdCast: Sealed + SimdElement {}

Why

A sealed trait can only be implemented within its defining crate. External crates can USE the trait but not IMPLEMENT it. This gives the library author freedom to add methods without breaking changes.

When to Use

Triggers:

  • You want to add trait methods in future versions without breaking downstream code
  • The set of valid implementors is fixed and known
  • You're publishing a library and need API stability guarantees

Example — before:

// Public trait — anyone can implement it
pub trait Codec {
    fn encode(&self, data: &[u8]) -> Vec<u8>;
    // Can NEVER add decode() without breaking all implementors
}

Example — after:

mod private { pub trait Sealed {} }

pub trait Codec: private::Sealed {
    fn encode(&self, data: &[u8]) -> Vec<u8>;
    fn decode(&self, data: &[u8]) -> Vec<u8>;  // safe to add later!
}

// Only YOUR crate can implement Codec because only YOUR crate
// can implement private::Sealed
impl private::Sealed for JsonCodec {}
impl Codec for JsonCodec { ... }

When NOT to Use

Don't use this when:

  • Users should be able to add their own implementations
  • You're writing application code (not a library)
  • The trait is intentionally an extension point

9. #[must_use] on Types and Functions

Source:

1,963 #[must_use] annotations across the stdlib.

#[must_use = "this `Result` may be an `Err` variant, which should be handled"]
pub enum Result<T, E> { ... }

#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct Map<I, F> { ... }

Why

The compiler warns when a #[must_use] value is ignored. This catches bugs where you call a function for its return value but forget to use it. Iterator adapters are all #[must_use] because they're lazy — creating a .map() does nothing until collected.

When to Use

Triggers:

  • Ignoring the return value is always a bug
  • The function/type is lazy (nothing happens without consumption)
  • Builder methods that return a new value (not &mut self)

Example — before:

fn sorted(mut v: Vec<i32>) -> Vec<i32> {
    v.sort();
    v
}

let data = vec![3, 1, 2];
sorted(data);  // BUG: result discarded, data is gone, nothing happened

Example — after:

#[must_use = "sorted returns the sorted vec; the original is consumed"]
fn sorted(mut v: Vec<i32>) -> Vec<i32> {
    v.sort();
    v
}

sorted(data);  // COMPILER WARNING: unused return value of `sorted`

When NOT to Use

Don't use this when:

  • The function is called for side effects (e.g., println!)
  • The return value is informational but not critical

10. Blanket Implementations

Source:

library/core/src/convert/mod.rs (From → Into blanket)

// Anyone who implements From gets Into for free:
impl<T, U> Into<U> for T where U: From<T> {
    fn into(self) -> U { U::from(self) }
}

// Every type can be converted from itself:
impl<T> From<T> for T {
    fn from(t: T) -> T { t }
}

Why

Blanket impls provide functionality to ALL types that satisfy a bound, without requiring each type to opt in. This is how Rust scales — you implement one trait and get many others for free.

When to Use

Triggers:

  • Trait B can be derived from trait A for ALL types
  • You want to reduce the number of impls users must write
  • There's a clear mathematical relationship (From ↔ Into)

When NOT to Use

Don't use this when:

  • The blanket would conflict with specific implementations
  • Not all types satisfying the bound actually want the blanket
  • The blanket would surprise users or violate expectations

Summary: Trait Design Decision Tree

Designing a new trait?
├── How many required methods?
│   ├── Can provide defaults built on a kernel → 1 required + many provided
│   └── Methods are independent → require each one
├── Who should implement it?
│   ├── Everyone → keep it simple, consider derive macro
│   ├── Only your crate → seal it (mod private { Sealed })
│   └── External + internal → leave it open, use #[non_exhaustive] on enums
├── Standard traits to implement on your types?
│   ├── Always: Debug
│   ├── If meaningful: Clone, PartialEq, Eq, Hash, Default
│   ├── If printable: Display
│   ├── If sequential: Iterator
│   ├── If wrapper: Deref
│   └── If convertible: From (not Into)
└── Should result be ignored?
    └── Never → add #[must_use]
When you need to... Use
Provide rich API from minimal impl One required method + provided defaults
Standard trait behavior #[derive(Debug, Clone, PartialEq, ...)]
Type conversion impl From<Source> for Target
Sensible zero/empty value impl Default (or derive)
Human-readable output impl Display
Iteration/composition impl Iterator
Smart pointer transparency impl Deref
Restrict implementors to your crate Sealed trait pattern
Prevent value discard #[must_use]
Free impls for all qualifying types Blanket implementation

See also: