388: Extension Trait Pattern
Tutorial Video
Text description (accessibility)
This video demonstrates the "388: Extension Trait Pattern" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. You want to add methods to a type you don't own — `str`, `Vec`, an external library type. Key difference from OCaml: 1. **Scoping**: Rust extension methods only appear when the trait is in scope (`use StrExt`); OCaml module functions are always available when the module is open.
Tutorial
The Problem
You want to add methods to a type you don't own — str, Vec, an external library type. Rust's orphan rule prevents implementing foreign traits on foreign types directly, but the extension trait pattern works around this: define a new trait in your crate, implement it for the foreign type, and bring it into scope with use. This is how itertools adds hundreds of methods to Iterator, how tokio adds async utilities to TcpStream, and how domain-specific libraries enrich standard library types.
The extension trait pattern is foundational to Rust's "composable without modification" philosophy and powers rayon's ParallelIterator, bytes::BufMut, and serde's derived impls.
🎯 Learning Outcomes
str, Vec<T>) in your crateuse MyExt brings extension methods into scope at the call siteCode Example
#![allow(clippy::all)]
//! Extension Trait Pattern
pub trait StrExt {
fn word_count(&self) -> usize;
fn capitalize_words(&self) -> String;
fn is_palindrome(&self) -> bool;
}
impl StrExt for str {
fn word_count(&self) -> usize {
self.split_whitespace().count()
}
fn capitalize_words(&self) -> String {
self.split_whitespace()
.map(|w| {
let mut c = w.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().to_string() + c.as_str(),
}
})
.collect::<Vec<_>>()
.join(" ")
}
fn is_palindrome(&self) -> bool {
let chars: Vec<char> = self.chars().collect();
chars.iter().eq(chars.iter().rev())
}
}
pub trait VecExt<T> {
fn second(&self) -> Option<&T>;
}
impl<T> VecExt<T> for Vec<T> {
fn second(&self) -> Option<&T> {
self.get(1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_word_count() {
assert_eq!("hello world".word_count(), 2);
}
#[test]
fn test_capitalize() {
assert_eq!("hello world".capitalize_words(), "Hello World");
}
#[test]
fn test_palindrome() {
assert!("racecar".is_palindrome());
assert!(!"hello".is_palindrome());
}
#[test]
fn test_second() {
assert_eq!(vec![1, 2, 3].second(), Some(&2));
}
}Key Differences
use StrExt); OCaml module functions are always available when the module is open.StrExt::word_count(&s)); OCaml resolves by module shadowing (last open wins).OCaml Approach
OCaml achieves extension through module inclusion: module StringExt = struct include String; let word_count s = ... end. This creates a new module wrapping the original with added functions. Unlike Rust's extension traits, OCaml's approach produces a new module rather than patching the original type's method namespace. There are no orphan restrictions since OCaml's type system doesn't tie methods to types.
Full Source
#![allow(clippy::all)]
//! Extension Trait Pattern
pub trait StrExt {
fn word_count(&self) -> usize;
fn capitalize_words(&self) -> String;
fn is_palindrome(&self) -> bool;
}
impl StrExt for str {
fn word_count(&self) -> usize {
self.split_whitespace().count()
}
fn capitalize_words(&self) -> String {
self.split_whitespace()
.map(|w| {
let mut c = w.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().to_string() + c.as_str(),
}
})
.collect::<Vec<_>>()
.join(" ")
}
fn is_palindrome(&self) -> bool {
let chars: Vec<char> = self.chars().collect();
chars.iter().eq(chars.iter().rev())
}
}
pub trait VecExt<T> {
fn second(&self) -> Option<&T>;
}
impl<T> VecExt<T> for Vec<T> {
fn second(&self) -> Option<&T> {
self.get(1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_word_count() {
assert_eq!("hello world".word_count(), 2);
}
#[test]
fn test_capitalize() {
assert_eq!("hello world".capitalize_words(), "Hello World");
}
#[test]
fn test_palindrome() {
assert!("racecar".is_palindrome());
assert!(!"hello".is_palindrome());
}
#[test]
fn test_second() {
assert_eq!(vec![1, 2, 3].second(), Some(&2));
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_word_count() {
assert_eq!("hello world".word_count(), 2);
}
#[test]
fn test_capitalize() {
assert_eq!("hello world".capitalize_words(), "Hello World");
}
#[test]
fn test_palindrome() {
assert!("racecar".is_palindrome());
assert!(!"hello".is_palindrome());
}
#[test]
fn test_second() {
assert_eq!(vec![1, 2, 3].second(), Some(&2));
}
}
Deep Comparison
OCaml vs Rust: 388-extension-trait-pattern
Exercises
NumExt trait and implement it for i32 with methods clamp_to_range(lo: i32, hi: i32) -> i32, digits() -> Vec<u8>, and is_prime() -> bool.OptionExt<T> that adds ok_or_log(msg: &str) -> Option<T> (logs a warning when None) and map_or_default<U: Default, F: Fn(T) -> U>(self, f: F) -> U.StatsExt trait for Iterator<Item = f64> that adds mean() -> Option<f64>, variance() -> Option<f64>, and histogram(buckets: usize) -> Vec<usize> methods, consuming the iterator.