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 |