Extensible Trait-Based APIs
Designing extensible trait-based APIs allows a library to add new convenience methods to existing types (Extension Traits) while controlling whether external downstream crates are permitted to implement a trait (Sealed Traits).
Two Complementary Trait Design Patterns:
1. Extension Trait Pattern (`pub trait SliceStatsExt: Sealed`):
Adds `.median()`, `.variance()` methods directly onto `[f64]` or `Iterator`!
Callers simply write `use my_crate::SliceStatsExt;` to unlock the methods.
2. Sealed Trait Pattern (`mod private { pub trait Sealed {} }`):
Prevents external crates from implementing `pub trait ByteOrder: private::Sealed`
so adding new required methods to `ByteOrder` in a minor release is NEVER a breaking change!
1. The Extension Trait Pattern (*Ext)
Rust's coherence rules (the Orphan Rule) prevent a library from adding inherent methods (impl<T> [T] { ... }) directly onto foreign types like [T], Iterator, Read, or Future.
Instead, define an Extension Trait (named *Ext by convention, such as Itertools, ReadBytesExt, or FutureExt) and provide a blanket implementation (impl<I: Iterator> MyIterExt for I):
/// Extension trait that adds a `.tap_each(...)` inspection adapter to every `Iterator`!
pub trait TapIteratorExt: Iterator + Sized {
fn tap_count(self, counter: &mut usize) -> TapCount<Self> {
TapCount {
inner: self,
counter,
}
}
}
/// Blanket implementation: every type implementing `Iterator` automatically gets `.tap_count()`!
impl<I: Iterator> TapIteratorExt for I {}
pub struct TapCount<'a, I> {
inner: I,
counter: &'a mut usize,
}
impl<I: Iterator> Iterator for TapCount<'_, I> {
type Item = I::Item;
fn next(&mut self) -> Option<Self::Item> {
let item = self.inner.next()?;
*self.counter += 1;
Some(item)
}
}
2. The Sealed Trait Pattern (Preventing External Implementations)
Normally, if you publish a pub trait FormatCodec, any downstream crate can write impl FormatCodec for TheirType. That means adding a new required method to FormatCodec later is a SemVer breaking change!
When a public trait is intended only to be implemented by types inside your own crate (for example, typestate markers, primitive integer bounds like u8/u16/u32/u64, or indexing overloads like std::slice::SliceIndex), seal the trait by giving it a supertrait private::Sealed located inside a private module:
- Because
pub trait Sealedsits inside a privatemod private, external crates cannot nameprivate::Sealedand therefore cannot implement your sealed trait! - Because external crates cannot implement the trait, you can add new methods to the trait in future minor releases (
1.1.0 -> 1.2.0) without breaking SemVer!
mod private {
/// Supertrait placed inside a private module: external crates cannot name or implement it!
pub trait Sealed {}
impl Sealed for u16 {}
impl Sealed for u32 {}
impl Sealed for u64 {}
}
/// A public trait that callers can use as a generic bound (`T: WireUnsigned`),
/// but that ONLY `u16`, `u32`, and `u64` can ever implement!
pub trait WireUnsigned: private::Sealed + Copy {
fn byte_width() -> usize;
fn to_u64(self) -> u64;
}
impl WireUnsigned for u16 {
fn byte_width() -> usize { 2 }
fn to_u64(self) -> u64 { u64::from(self) }
}
impl WireUnsigned for u32 {
fn byte_width() -> usize { 4 }
fn to_u64(self) -> u64 { u64::from(self) }
}
impl WireUnsigned for u64 {
fn byte_width() -> usize { 8 }
fn to_u64(self) -> u64 { self }
}
pub fn describe_wire_int<T: WireUnsigned>(val: T) -> String {
format!("{} ({} bytes)", val.to_u64(), T::byte_width())
}
fn main() {
// 1. Test Extension Trait (`TapIteratorExt`):
let mut visited = 0usize;
let sum: i32 = [10, 20, 30, 40]
.into_iter()
.tap_count(&mut visited)
.take(3)
.sum();
assert_eq!(sum, 60);
assert_eq!(visited, 3);
// 2. Test Sealed Trait (`WireUnsigned`):
println!(
"Visited {} items (sum={sum}); WireUnsigned u32: {}",
visited,
describe_wire_int(42u32)
);
}
Visited 3 items (sum=60); WireUnsigned u32: 42 (4 bytes)