Higher-Ranked Trait Bounds
In generic and type-level programming, Higher-Ranked Trait Bounds (HRTBs, for<'a>) combine with Generic Associated Types (GATs), type-level adapters, and trait projections to express constraints over lifetime-parameterized associated types for all possible lifetimes 'a.
Combining HRTBs with Generic Associated Types (for<'a> I::Item<'a>: Trait)
When writing a generic algorithm over a trait with a lifetime GAT (such as LendingIterator), you often need to require that the yielded associated type I::Item<'a> implements Debug, Display, or another trait for every borrow lifetime 'a produced inside the loop.
Writing where for<'a> I: LendingIterator, for<'a> <I as LendingIterator>::Item<'a>: Debug (or where I: LendingIterator, for<'a> I::Item<'a>: Debug) universally quantifies the bound across all loop iterations:
use std::fmt::Debug;
trait LendingSource {
type View<'a>: Debug
where
Self: 'a;
fn next_view(&mut self) -> Option<Self::View<'_>>;
}
struct LineBuffer {
lines: Vec<String>,
index: usize,
}
impl LendingSource for LineBuffer {
type View<'a>
= &'a str
where
Self: 'a;
fn next_view(&mut self) -> Option<Self::View<'_>> {
let line = self.lines.get(self.index)?;
self.index += 1;
Some(line.as_str())
}
}
// Generic consumer constraining `S::View<'a>` across all lifetimes `'a`:
fn dump_all<S>(source: &mut S)
where
S: LendingSource,
for<'a> S::View<'a>: Debug,
{
while let Some(view) = source.next_view() {
println!("view: {:?}", view);
}
}
fn main() {
let mut buf = LineBuffer {
lines: vec![String::from("header"), String::from("payload")],
index: 0,
};
dump_all(&mut buf);
}
view: "header"
view: "payload"
Higher-Ranked Trait Objects (dyn for<'a> Trait<'a>)
A trait object type can itself be higher-ranked over a lifetime parameter: Box<dyn for<'a> Validator<'a>>. Whereas Box<dyn Validator<'static>> can only validate 'static strings, Box<dyn for<'a> Validator<'a>> can be invoked dynamically with short-lived stack strings of any lifetime 'a:
trait Validator<'a> {
fn validate(&self, input: &'a str) -> Result<&'a str, &'static str>;
}
struct NonEmptyTrimmed;
impl<'a> Validator<'a> for NonEmptyTrimmed {
fn validate(&self, input: &'a str) -> Result<&'a str, &'static str> {
let trimmed = input.trim();
if trimmed.is_empty() {
Err("empty after trim")
} else {
Ok(trimmed)
}
}
}
fn main() {
let rules: Vec<Box<dyn for<'a> Validator<'a>>> = vec![Box::new(NonEmptyTrimmed)];
let local_input = String::from(" clean-token ");
for rule in &rules {
println!("validated: {:?}", rule.validate(&local_input));
}
}
validated: Ok("clean-token")
Comparing Fixed-Lifetime Bounds and Higher-Ranked Bounds in Generics
| Bound Syntax | Who Chooses 'a? |
Can 'a Be a Local Borrow Inside the Callee? |
|---|---|---|
fn f<'a, T: Trait<'a>>(x: T) |
Caller (fixed before f starts) |
No ('a outlives the entire body of f) |
fn f<T>(x: T) where for<'a> T: Trait<'a> |
Callee (f can instantiate 'a at any inner block) |
Yes (T implements Trait for every lifetime) |
Box<dyn Trait<'a>> |
Bound to outer lifetime 'a |
No |
Box<dyn for<'a> Trait<'a>> |
Instantiated per dynamic method call | Yes |