Associated Types and Advanced Traits

Advanced Trait API Design

Designing expressive, zero-cost trait APIs in Rust combines extension methods, iterator/adapter combinators, Sized-gated methods for dyn compatibility, and type-erased wrappers when compile-time generics meet dynamic boundaries.

Designing Combinator Chains via Extension Traits

Just as Iterator provides .map() and .filter() that wrap Self in small adapter structs (Map<Self, F>, Filter<Self, P>), custom traits can provide composable combinators by returning adapter structs that themselves implement the trait:

pub trait Validator<T: ?Sized> {
    fn is_valid(&self, input: &T) -> bool;

    fn and<Other>(self, other: Other) -> AndValidator<Self, Other>
    where
        Self: Sized,
        Other: Validator<T>,
    {
        AndValidator {
            first: self,
            second: other,
        }
    }
}

pub struct AndValidator<A, B> {
    first: A,
    second: B,
}

impl<T: ?Sized, A: Validator<T>, B: Validator<T>> Validator<T> for AndValidator<A, B> {
    fn is_valid(&self, input: &T) -> bool {
        self.first.is_valid(input) && self.second.is_valid(input)
    }
}

struct MinLen(usize);

impl Validator<str> for MinLen {
    fn is_valid(&self, input: &str) -> bool {
        input.len() >= self.0
    }
}

struct StartsWith(&'static str);

impl Validator<str> for StartsWith {
    fn is_valid(&self, input: &str) -> bool {
        input.starts_with(self.0)
    }
}

fn main() {
    // Composes two validators statically without heap allocation:
    let rule = StartsWith("pk_").and(MinLen(8));

    println!("'pk_123456' valid: {}", rule.is_valid("pk_123456"));
    println!("'pk_12' valid:     {}", rule.is_valid("pk_12"));
    println!("'sk_123456' valid: {}", rule.is_valid("sk_123456"));
}
'pk_123456' valid: true
'pk_12' valid:     false
'sk_123456' valid: false

Because Validator::and carries a where Self: Sized bound, Validator<str> remains dyn-compatible (Box<dyn Validator<str>>) while still offering ergonomic by-value chaining for concrete types.

Bridging Static and Dynamic Dispatch: Implementing a Trait for Box<dyn Trait>

When a trait is used both with static generics (fn check<V: Validator<str>>(v: V)) and dynamic trait objects (Box<dyn Validator<str>>), implementing the trait for Box<T> (where T: Validator + ?Sized) and &T lets callers pass a Box<dyn Validator<str>> into any generic function expecting impl Validator<str>:

trait Formatter {
    fn format_line(&self, msg: &str) -> String;
}

impl<F: Formatter + ?Sized> Formatter for Box<F> {
    fn format_line(&self, msg: &str) -> String {
        (**self).format_line(msg)
    }
}

struct BracketFormatter;

impl Formatter for BracketFormatter {
    fn format_line(&self, msg: &str) -> String {
        format!("[{msg}]")
    }
}

fn emit(fmt: impl Formatter, text: &str) {
    println!("{}", fmt.format_line(text));
}

fn main() {
    let boxed: Box<dyn Formatter> = Box::new(BracketFormatter);
    // Works because Box<F> implements Formatter for F: Formatter + ?Sized:
    emit(boxed, "ready");
}
[ready]

Checklist for Production Trait APIs

Design Decision Recommended Practice
Unsized target support Add + ?Sized to type parameters and Box<F> / &F forwarding impls where values are only accessed by reference
Preserving dyn compatibility Add where Self: Sized to by-value builder/combinator methods on traits
Borrowing vs. ownership Implement traits for &T, &mut T, and Box<T> so generic functions taking T by value also accept borrowed or boxed trait objects
Evolution safety Provide default implementations for secondary methods, or seal traits that should not be implemented outside the crate