Nugget 7
Home
You wish "hello@example.com" had an .is_email() method,
but &str is a built-in type. Extension traits let you add
methods to any type — even ones from other crates.
You can't add methods to &str or String directly —
they belong to the standard library:
// You want:
"hello@example.com".is_email(); // ❌ no such method on &str
// So you write a free function instead:
fn is_email(s: &str) -> bool {
s.contains('@')
}
// But call sites are less readable:
if is_email(&user_input) { /* ... */ }
Free functions work, but method syntax (user_input.is_email())
reads more naturally in many cases.
Define a trait with the method you want, then implement it for the target type.
When you bring the trait into scope with use, the method appears
on the type:
// 1. Define a trait
trait EmailCheck {
fn is_email(&self) -> bool;
}
// 2. Implement it for &str
impl EmailCheck for str {
fn is_email(&self) -> bool {
self.contains('@') && self.contains('.')
}
}
// 3. Use it — just bring the trait into scope
fn main() {
let s = "hello@example.com";
println!("{}", s.is_email()); // ✅ true
let bad = "not-an-email";
println!("{}", bad.is_email()); // ✅ false
}
📐 Note: str vs &str
We implement for str (the unsized type), not &str
(a reference). &self in the trait becomes a reference to
str, which is &str. This applies to any
method taking &self.
Extension traits are especially useful for common utility methods:
// Extending Vec with a shorthand
trait VecUtils<T> {
fn first_or<'a>(&'a self, default: &'a T) -> &'a T;
}
impl<T> VecUtils<T> for Vec<T> {
fn first_or<'a>(&'a self, default: &'a T) -> &'a T {
self.first().unwrap_or(default)
}
}
fn main() {
let v: Vec<i32> = vec![];
let n = v.first_or(&42);
println!("{}", n); // 42
}
// Extending Option for convenience
trait OptionStringExt {
fn unwrap_or_empty(self) -> String;
}
impl OptionStringExt for Option<String> {
fn unwrap_or_empty(self) -> String {
self.unwrap_or_default()
}
}
fn main() {
let name: Option<String> = None;
println!("Hello, {}!", name.unwrap_or_empty()); // Hello, !
}
itertoolsThe most famous extension trait in the Rust ecosystem is itertools::Itertools. It adds dozens of extra methods to all iterators:
use itertools::Itertools; // bring extension trait into scope
fn main() {
let chars = "hello"
.chars()
.unique() // ✅ not on standard Iterator
.sorted() // ✅ not on standard Iterator
.join(", "); // ✅ not on standard Iterator
println!("{}", chars); // e, h, l, o
}
Itertools is a trait. You use it, and suddenly every
iterator in your code gains those methods.
Rust has an "orphan rule": you can implement a trait for a type only if you own either the trait or the type (or both). This prevents conflicting implementations across crates:
// ✅ You own both the trait and the type
impl EmailCheck for str { /* ... */ }
// ✅ You own the type, implementing a std trait
impl From<MyType> for String { /* ... */ }
// ❌ Neither is yours — blocked by orphan rule
impl Display for Vec<String> { /* ... */ }
// (Display is std, Vec is std — won't compile)
💡 The newtype pattern
To work around the orphan rule, wrap the foreign type in a tuple struct:
struct MyVec(Vec<String>). Now you "own" the outer type and
can implement any trait on it.
use it to activate.itertools crate is the classic example — Itertools extends every iterator.