Home/Advanced Trait System/Higher-Ranked Trait Bounds

Higher-Ranked Trait Bounds

for<'a> patterns and HRTBs

expert
hrtblifetimesadvanced
🎮 Interactive Playground

What Are Higher-Ranked Trait Bounds?

Higher-Ranked Trait Bounds (HRTBs) are constraints that require a trait implementation to work for all possible lifetimes, not just one specific lifetime. They use the for<'a> syntax to express "for any lifetime 'a, this trait bound must hold."

The problem HRTBs solve: Sometimes you need a generic type (like a closure) that can accept references with any lifetime, not just one specific lifetime chosen by the caller. Regular lifetime parameters are chosen once; HRTBs require the trait to work for all lifetime choices.
// Regular lifetime parameter - chosen once by caller
fn call_with_str<'a, F>(data: &'a str, f: F) 
where 
    F: Fn(&'a str) -> bool  // F works with THIS specific 'a
{
    f(data);
}

// Higher-Ranked Trait Bound - must work for ALL lifetimes
fn call_with_any_str<F>(f: F)
where
    F: for<'a> Fn(&'a str) -> bool  // F works with ANY 'a
{
    let short_lived = String::from("temporary");
    f(&short_lived);  // F must accept this short lifetime
    
    f("static string");  // AND this 'static lifetime
}
Why they exist: Lifetimes are type parameters, and like types, they have "rank":
  • Rank 0: Concrete lifetimes like 'static or specific named lifetimes
  • Rank 1: Generic lifetime parameters <'a>
  • Rank 2+: Universal quantification over lifetimes for<'a>

HRTBs are "higher-ranked" because they quantify over all possible lifetime choices.

When HRTBs Are Required

The Rust compiler implicitly uses HRTBs in many common scenarios, but sometimes you must write them explicitly:

// IMPLICIT HRTB - Compiler infers for<'a>
fn takes_fn_ref(f: impl Fn(&str) -> usize) {
    // Implicitly: for<'a> Fn(&'a str) -> usize
}

// EXPLICIT HRTB - Required in trait bounds and where clauses
fn explicit_version<F>(f: F)
where
    F: for<'a> Fn(&'a str) -> usize
{
    // Same meaning, but explicit
}

// ERROR: This doesn't work - wrong lifetime rank
fn broken_version<'a, F>(f: F)
where
    F: Fn(&'a str) -> usize  // 'a is chosen once, too restrictive
{
    let s1 = String::from("hello");
    f(&s1);  // OK
    
    let s2 = String::from("world");
    f(&s2);  // ERROR: s2 has different lifetime than 'a
}

Real-World Example 1: Validation and Filtering (Systems Programming)

HRTBs shine when you need functions that accept closures working with borrowed data:

use std::collections::HashMap;

/// A validation system that checks data without taking ownership
pub struct Validator<F>
where
    F: for<'a> Fn(&'a str) -> bool,
{
    predicate: F,
    name: String,
}

impl<F> Validator<F>
where
    F: for<'a> Fn(&'a str) -> bool,
{
    pub fn new(name: impl Into<String>, predicate: F) -> Self {
        Self {
            predicate,
            name: name.into(),
        }
    }

    /// Validate data from any source with any lifetime
    pub fn validate(&self, data: &str) -> bool {
        (self.predicate)(data)
    }

    /// Validate multiple strings with different lifetimes
    pub fn validate_all(&self, items: &[String]) -> Vec<bool> {
        items.iter()
            .map(|s| self.validate(s))  // Each &str has different lifetime
            .collect()
    }

    /// Filter a collection based on validation
    pub fn filter_valid<'a>(&self, items: &'a [String]) -> Vec<&'a str> {
        items.iter()
            .filter(|s| self.validate(s))
            .map(|s| s.as_str())
            .collect()
    }
}

// Real-world usage: Configuration validation
pub struct ConfigValidator {
    validators: HashMap<String, Box<dyn Fn(&str) -> bool>>,
}

impl ConfigValidator {
    pub fn new() -> Self {
        Self {
            validators: HashMap::new(),
        }
    }

    /// Add a validator - notice the HRTB in the trait object
    pub fn add_validator<F>(&mut self, field: String, validator: F)
    where
        F: for<'a> Fn(&'a str) -> bool + 'static,
    {
        self.validators.insert(field, Box::new(validator));
    }

    /// Validate a configuration field
    pub fn validate_field(&self, field: &str, value: &str) -> Result<(), String> {
        match self.validators.get(field) {
            Some(validator) => {
                if validator(value) {
                    Ok(())
                } else {
                    Err(format!("Validation failed for field: {}", field))
                }
            }
            None => Err(format!("No validator for field: {}", field)),
        }
    }
}

// Example usage
fn main() {
    // Create validators with different predicates
    let email_validator = Validator::new(
        "email",
        |s: &str| s.contains('@') && s.contains('.'),
    );

    let port_validator = Validator::new(
        "port",
        |s: &str| s.parse::<u16>().is_ok(),
    );

    // Validate data from different scopes with different lifetimes
    {
        let temp_email = String::from("user@example.com");
        assert!(email_validator.validate(&temp_email));
    } // temp_email dropped

    assert!(email_validator.validate("admin@test.org"));  // 'static

    // Validate a collection
    let ports = vec![
        String::from("8080"),
        String::from("invalid"),
        String::from("443"),
    ];

    let valid_ports = port_validator.filter_valid(&ports);
    assert_eq!(valid_ports, vec!["8080", "443"]);

    // Config validation system
    let mut config_validator = ConfigValidator::new();
    
    config_validator.add_validator(
        "email".to_string(),
        |s| s.contains('@'),
    );
    
    config_validator.add_validator(
        "port".to_string(),
        |s| s.parse::<u16>().map(|p| p > 0 && p < 65536).unwrap_or(false),
    );

    // Validate from different sources
    let user_input = String::from("8080");
    assert!(config_validator.validate_field("port", &user_input).is_ok());
    
    assert!(config_validator.validate_field("port", "80").is_ok());  // 'static
}
Why HRTB is required here:

The validator must work with &str references from:

  • Temporary String values (short lifetimes)
  • Static string literals (lifetime 'static)
  • String slices from various data structures (different lifetimes)

Without for<'a>, we'd need to specify a single lifetime 'a that covers all uses, which is impossible when validating data from different scopes.

Real-World Example 2: Database Transactions (Web/Backend)

Database connection traits are a classic HRTB use case - transactions need to work with queries of any lifetime:

use std::error::Error;
use std::fmt;

#[derive(Debug)]
pub struct DbError(String);

impl fmt::Display for DbError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Database error: {}", self.0)
    }
}

impl Error for DbError {}

/// A row from a database query
#[derive(Debug, Clone)]
pub struct Row {
    data: Vec<String>,
}

impl Row {
    pub fn get(&self, index: usize) -> Option<&str> {
        self.data.get(index).map(|s| s.as_str())
    }
}

/// Transaction trait that can execute queries with borrowed SQL strings
/// HRTB required: queries can come from various sources with different lifetimes
pub trait Transaction {
    /// Execute a query with any lifetime SQL string
    fn execute(&mut self, sql: &str) -> Result<Vec<Row>, DbError>;

    /// Execute within a callback - the callback must work for ANY lifetime
    fn with_query<F, R>(&mut self, sql: &str, f: F) -> Result<R, DbError>
    where
        F: for<'a> FnOnce(&'a [Row]) -> R;
}

/// Comparison: WITHOUT HRTB (doesn't work)
/// 
/// pub trait BrokenTransaction<'sql> {
///     fn execute(&mut self, sql: &'sql str) -> Result<Vec<Row>, DbError>;
/// }
///
/// Problem: The 'sql lifetime is fixed when you create the transaction,
/// but we need to execute multiple queries with different lifetimes!

/// A mock database transaction
pub struct MockTransaction {
    committed: bool,
}

impl MockTransaction {
    pub fn new() -> Self {
        Self { committed: false }
    }

    pub fn commit(&mut self) -> Result<(), DbError> {
        self.committed = true;
        Ok(())
    }
}

impl Transaction for MockTransaction {
    fn execute(&mut self, sql: &str) -> Result<Vec<Row>, DbError> {
        // Mock implementation - in reality, would parse and execute SQL
        if sql.contains("SELECT") {
            Ok(vec![
                Row { data: vec!["1".to_string(), "Alice".to_string()] },
                Row { data: vec!["2".to_string(), "Bob".to_string()] },
            ])
        } else {
            Ok(vec![])
        }
    }

    fn with_query<F, R>(&mut self, sql: &str, f: F) -> Result<R, DbError>
    where
        F: for<'a> FnOnce(&'a [Row]) -> R,
    {
        let rows = self.execute(sql)?;
        Ok(f(&rows))
    }
}

/// A database connection pool that provides transactions
pub struct ConnectionPool;

impl ConnectionPool {
    pub fn new() -> Self {
        Self
    }

    /// Execute work in a transaction - closure must accept ANY transaction lifetime
    pub fn with_transaction<F, R>(&self, work: F) -> Result<R, Box<dyn Error>>
    where
        F: for<'a> FnOnce(&'a mut dyn Transaction) -> Result<R, DbError>,
    {
        let mut txn = MockTransaction::new();
        let result = work(&mut txn)?;
        txn.commit()?;
        Ok(result)
    }
}

// Real-world usage example
fn main() -> Result<(), Box<dyn Error>> {
    let pool = ConnectionPool::new();

    // Execute multiple queries with different SQL string lifetimes
    let result = pool.with_transaction(|txn| {
        // Query from a static string
        let users = txn.execute("SELECT * FROM users")?;
        println!("Found {} users", users.len());

        // Query from a temporary String (different lifetime)
        {
            let table = String::from("orders");
            let query = format!("SELECT * FROM {}", table);
            let orders = txn.execute(&query)?;
            println!("Found {} orders", orders.len());
        } // query and table dropped here

        // Query with callback processing
        let count = txn.with_query("SELECT * FROM products", |rows| {
            rows.iter().filter(|r| r.get(0).is_some()).count()
        })?;

        Ok(count)
    })?;

    println!("Final result: {}", result);

    // Another transaction with completely different query lifetimes
    pool.with_transaction(|txn| {
        let query1 = String::from("SELECT id FROM users");
        txn.execute(&query1)?;

        let query2 = String::from("SELECT id FROM orders");
        txn.execute(&query2)?;

        Ok(())
    })?;

    Ok(())
}
Why HRTB is crucial here:
  1. Multiple queries per transaction: Each query string has a different lifetime
  2. Query building: SQL strings might be static literals or dynamically built
  3. Callback patterns: with_query accepts closures that must work with any row lifetime
  4. Transaction pooling: The transaction reference itself has a lifetime independent of query lifetimes

Without HRTB, you'd need to specify all query lifetimes upfront, making the API unusable.

Real-World Example 3: Parser Combinators (Network/Protocol Parsing)

Parser combinators are the canonical HRTB use case - parsers must handle input slices with any lifetime:

use std::error::Error;
use std::fmt;

/// Parse result: remaining input and parsed value, or error
pub type ParseResult<'a, T> = Result<(&'a [u8], T), ParseError>;

#[derive(Debug, Clone)]
pub struct ParseError {
    message: String,
}

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Parse error: {}", self.message)
    }
}

impl Error for ParseError {}

/// A parser is a function that takes input and returns a result
/// CRITICAL: for<'a> is required because the parser must work with
/// input slices of ANY lifetime, not just one specific lifetime
pub trait Parser<Output> {
    fn parse<'a>(&self, input: &'a [u8]) -> ParseResult<'a, Output>;
}

// Function-based parser using HRTB
impl<F, Output> Parser<Output> for F
where
    F: for<'a> Fn(&'a [u8]) -> ParseResult<'a, Output>,
{
    fn parse<'a>(&self, input: &'a [u8]) -> ParseResult<'a, Output> {
        self(input)
    }
}

/// Parse a single byte matching a predicate
pub fn byte_where<F>(predicate: F) -> impl Parser<u8>
where
    F: Fn(u8) -> bool,
{
    move |input: &[u8]| {
        if input.is_empty() {
            Err(ParseError {
                message: "Unexpected end of input".to_string(),
            })
        } else if predicate(input[0]) {
            Ok((&input[1..], input[0]))
        } else {
            Err(ParseError {
                message: format!("Byte {} did not match predicate", input[0]),
            })
        }
    }
}

/// Parse a specific byte
pub fn byte(expected: u8) -> impl Parser<u8> {
    byte_where(move |b| b == expected)
}

/// Parse a sequence of bytes (literal string)
pub fn tag(expected: &'static [u8]) -> impl Parser<&'static [u8]> {
    move |input: &[u8]| {
        if input.len() < expected.len() {
            return Err(ParseError {
                message: format!("Expected {:?}, got end of input", expected),
            });
        }

        if &input[..expected.len()] == expected {
            Ok((&input[expected.len()..], expected))
        } else {
            Err(ParseError {
                message: format!(
                    "Expected {:?}, got {:?}",
                    expected,
                    &input[..expected.len().min(input.len())]
                ),
            })
        }
    }
}

/// Map parser output - demonstrates HRTB in combinator
pub fn map<P, F, A, B>(parser: P, f: F) -> impl Parser<B>
where
    P: Parser<A>,
    F: Fn(A) -> B,
{
    move |input: &[u8]| {
        let (remaining, value) = parser.parse(input)?;
        Ok((remaining, f(value)))
    }
}

/// Parse two items in sequence
pub fn pair<P1, P2, A, B>(parser1: P1, parser2: P2) -> impl Parser<(A, B)>
where
    P1: Parser<A>,
    P2: Parser<B>,
{
    move |input: &[u8]| {
        let (remaining, value1) = parser1.parse(input)?;
        let (remaining, value2) = parser2.parse(remaining)?;
        Ok((remaining, (value1, value2)))
    }
}

/// Parse one of two alternatives
pub fn alt<P1, P2, A>(parser1: P1, parser2: P2) -> impl Parser<A>
where
    P1: Parser<A>,
    P2: Parser<A>,
{
    move |input: &[u8]| parser1.parse(input).or_else(|_| parser2.parse(input))
}

/// Parse zero or more repetitions
pub fn many0<P, A>(parser: P) -> impl Parser<Vec<A>>
where
    P: Parser<A>,
{
    move |mut input: &[u8]| {
        let mut results = Vec::new();
        
        loop {
            match parser.parse(input) {
                Ok((remaining, value)) => {
                    results.push(value);
                    input = remaining;
                }
                Err(_) => break,
            }
        }

        Ok((input, results))
    }
}

// Real-world protocol parser: Simple HTTP request line
pub fn http_method() -> impl Parser<&'static str> {
    move |input: &[u8]| {
        alt(
            map(tag(b"GET"), |_| "GET"),
            alt(
                map(tag(b"POST"), |_| "POST"),
                alt(
                    map(tag(b"PUT"), |_| "PUT"),
                    map(tag(b"DELETE"), |_| "DELETE"),
                ),
            ),
        )
        .parse(input)
    }
}

pub fn parse_until_space() -> impl Parser<Vec<u8>> {
    many0(byte_where(|b| b != b' '))
}

pub fn http_request_line() -> impl Parser<(String, String, String)> {
    move |input: &[u8]| {
        let (input, method) = http_method().parse(input)?;
        let (input, _) = byte(b' ').parse(input)?;
        let (input, path) = parse_until_space().parse(input)?;
        let (input, _) = byte(b' ').parse(input)?;
        let (input, version) = parse_until_space().parse(input)?;

        Ok((
            input,
            (
                method.to_string(),
                String::from_utf8_lossy(&path).to_string(),
                String::from_utf8_lossy(&version).to_string(),
            ),
        ))
    }
}

fn main() {
    // Parse from a static buffer
    let request1 = b"GET /index.html HTTP/1.1";
    match http_request_line().parse(request1) {
        Ok((remaining, (method, path, version))) => {
            println!("Method: {}, Path: {}, Version: {}", method, path, version);
            println!("Remaining: {:?}", remaining);
        }
        Err(e) => println!("Error: {}", e),
    }

    // Parse from a temporary buffer (different lifetime)
    {
        let temp_buffer = b"POST /api/users HTTP/1.1".to_vec();
        match http_request_line().parse(&temp_buffer) {
            Ok((_, (method, path, version))) => {
                println!("Method: {}, Path: {}, Version: {}", method, path, version);
            }
            Err(e) => println!("Error: {}", e),
        }
    } // temp_buffer dropped

    // Parse from another temporary buffer
    let another_buffer = String::from("DELETE /api/users/123 HTTP/2.0");
    match http_request_line().parse(another_buffer.as_bytes()) {
        Ok((_, (method, path, version))) => {
            println!("Method: {}, Path: {}, Version: {}", method, path, version);
        }
        Err(e) => println!("Error: {}", e),
    }
}
Why HRTB is essential for parsers:
  1. Input from anywhere: Parsers must handle input buffers with any lifetime (static, heap, stack)
  2. Chaining parsers: Each parser returns a slice with the same lifetime as input
  3. Combinator composition: Higher-order parsers like map, pair, many0 must work regardless of input lifetime
  4. Zero-copy parsing: Output references often point into the input buffer, preserving lifetimes

The signature for<'a> Fn(&'a [u8]) -> ParseResult<'a, Output> is the heart of parser combinators.

Real-World Example 4: Async Middleware (Async/Concurrency)

Async middleware systems require HRTBs when handlers must work with borrowed request data:

use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;

/// HTTP Request with borrowed body
#[derive(Clone)]
pub struct Request<'a> {
    pub method: &'a str,
    pub path: &'a str,
    pub body: &'a [u8],
}

/// HTTP Response
#[derive(Clone)]
pub struct Response {
    pub status: u16,
    pub body: Vec<u8>,
}

impl Response {
    pub fn ok(body: Vec<u8>) -> Self {
        Self { status: 200, body }
    }

    pub fn not_found() -> Self {
        Self {
            status: 404,
            body: b"Not Found".to_vec(),
        }
    }
}

/// Middleware trait - must handle requests with ANY lifetime
/// HRTB required: middleware shouldn't care about request lifetime
pub trait Middleware: Send + Sync {
    fn call<'a>(
        &'a self,
        req: Request<'a>,
    ) -> Pin<Box<dyn Future<Output = Response> + Send + 'a>>;
}

/// Function-based middleware using HRTB
impl<F, Fut> Middleware for F
where
    F: Fn(Request<'_>) -> Fut + Send + Sync,
    Fut: Future<Output = Response> + Send,
{
    fn call<'a>(
        &'a self,
        req: Request<'a>,
    ) -> Pin<Box<dyn Future<Output = Response> + Send + 'a>> {
        Box::pin(self(req))
    }
}

/// Middleware stack that chains multiple middleware
pub struct MiddlewareStack {
    middleware: Vec<Arc<dyn Middleware>>,
}

impl MiddlewareStack {
    pub fn new() -> Self {
        Self {
            middleware: Vec::new(),
        }
    }

    pub fn add<M: Middleware + 'static>(&mut self, m: M) {
        self.middleware.push(Arc::new(m));
    }

    pub async fn handle<'a>(&self, req: Request<'a>) -> Response {
        // In a real implementation, middleware would be chained
        // For simplicity, we just call the first middleware
        if let Some(m) = self.middleware.first() {
            m.call(req).await
        } else {
            Response::not_found()
        }
    }
}

// Logging middleware
async fn logging_middleware(req: Request<'_>) -> Response {
    println!("Request: {} {}", req.method, req.path);
    
    // Simulate some async work
    tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
    
    Response::ok(b"Logged".to_vec())
}

// Authentication middleware with HRTB
fn auth_middleware<F>(check_auth: F) -> impl Middleware
where
    F: for<'a> Fn(&'a Request<'a>) -> bool + Send + Sync,
{
    move |req: Request<'_>| async move {
        if check_auth(&req) {
            Response::ok(b"Authenticated".to_vec())
        } else {
            Response {
                status: 401,
                body: b"Unauthorized".to_vec(),
            }
        }
    }
}

// Rate limiting middleware
struct RateLimiter {
    max_requests: usize,
}

impl Middleware for RateLimiter {
    fn call<'a>(
        &'a self,
        req: Request<'a>,
    ) -> Pin<Box<dyn Future<Output = Response> + Send + 'a>> {
        Box::pin(async move {
            // Check rate limit (simplified)
            if req.path.contains("limited") {
                Response {
                    status: 429,
                    body: b"Too Many Requests".to_vec(),
                }
            } else {
                Response::ok(b"Within rate limit".to_vec())
            }
        })
    }
}

#[tokio::main]
async fn main() {
    let mut stack = MiddlewareStack::new();

    // Add logging middleware
    stack.add(logging_middleware);

    // Add authentication middleware with custom checker
    stack.add(auth_middleware(|req: &Request| {
        // Check for auth header in path (simplified)
        req.path.contains("authenticated")
    }));

    // Add rate limiter
    stack.add(RateLimiter { max_requests: 100 });

    // Handle requests with different lifetimes
    {
        let method = String::from("GET");
        let path = String::from("/api/authenticated");
        let body = vec![1, 2, 3];

        let req = Request {
            method: &method,
            path: &path,
            body: &body,
        };

        let resp = stack.handle(req).await;
        println!("Response: {}", resp.status);
    } // Temporary data dropped

    // Static request data
    let req2 = Request {
        method: "POST",
        path: "/api/limited",
        body: b"data",
    };

    let resp2 = stack.handle(req2).await;
    println!("Response: {}", resp2.status);
}
Why HRTB matters for async middleware:
  1. Request lifetime independence: Middleware shouldn't constrain request data lifetime
  2. Composability: Multiple middleware can be chained without lifetime conflicts
  3. Flexibility: Requests can be constructed from various sources (heap, stack, static)
  4. Async traits: Combining async with trait methods requires careful lifetime handling

The pattern for<'a> Fn(Request<'a>) -> Future allows middleware to accept requests with any lifetime while returning futures tied to that lifetime.

Deep Dive: Understanding HRTB Mechanics

Lifetime Rank Explained

Lifetimes in Rust have a "rank" similar to type polymorphism:

// Rank 0: Concrete lifetime (monomorphic)
fn rank_0(data: &'static str) -> usize {
    data.len()
}

// Rank 1: Generic lifetime parameter (polymorphic)
fn rank_1<'a>(data: &'a str) -> usize {
    data.len()
}

// Rank 2: Universal quantification (higher-ranked)
fn rank_2<F>(f: F) -> usize
where
    F: for<'a> Fn(&'a str) -> usize,
{
    f("any lifetime") + f("another lifetime")
}
Key difference:
  • Rank 1: Caller chooses the lifetime once
  • Rank 2: Function can call with multiple different lifetimes

When HRTB is Implicit vs Explicit

The Rust compiler automatically adds HRTB in certain contexts:

// Implicit HRTB - compiler infers for<'a>
fn implicit_fn(f: impl Fn(&str) -> bool) {
    // f has type: for<'a> Fn(&'a str) -> bool
}

// Implicit in trait object
let f: Box<dyn Fn(&str) -> bool> = Box::new(|s| s.is_empty());
// Actually: Box<dyn for<'a> Fn(&'a str) -> bool>

// MUST be explicit in where clauses
fn explicit_where<F>(f: F)
where
    F: for<'a> Fn(&'a str) -> bool,  // Required!
{
    // ...
}

// MUST be explicit in trait bounds with type parameters
trait MyTrait<F>
where
    F: for<'a> Fn(&'a str) -> bool,  // Required!
{
    fn call(&self, f: &F);
}

// MUST be explicit in struct fields
struct MyStruct<F>
where
    F: for<'a> Fn(&'a str) -> bool,  // Required!
{
    callback: F,
}

Error Messages Without HRTB

Understanding error messages helps identify when HRTB is needed:

// ERROR: This won't compile
fn broken_example<'a, F>(f: F)
where
    F: Fn(&'a str) -> bool,
{
    let s1 = String::from("hello");
    f(&s1);  // ERROR: 's1' lifetime is not 'a
    
    let s2 = String::from("world");
    f(&s2);  // ERROR: 's2' lifetime is not 'a
}

// Compiler error:
// error[E0597]: `s1` does not live long enough
//   --> src/main.rs:5:7
//    |
// 4 |     let s1 = String::from("hello");
//    |         -- binding `s1` declared here
// 5 |     f(&s1);
//    |       ^^^ borrowed value does not live long enough
//    |
//    = note: the closure requires that `s1` is borrowed for `'a`
//    but `s1` only lives until the end of this block
The fix: Use HRTB so F can accept any lifetime, not just 'a:
fn fixed_example<F>(f: F)
where
    F: for<'a> Fn(&'a str) -> bool,
{
    let s1 = String::from("hello");
    f(&s1);  // OK: F accepts any lifetime, including s1's

    let s2 = String::from("world");
    f(&s2);  // OK: F accepts s2's lifetime too
}

HRTB with Multiple Lifetimes

HRTBs can quantify over multiple lifetimes:

// Function accepting closure with two independent lifetimes
fn two_lifetimes<F>(f: F)
where
    F: for<'a, 'b> Fn(&'a str, &'b str) -> bool,
{
    let s1 = String::from("hello");
    let s2 = String::from("world");
    
    f(&s1, &s2);  // 'a and 'b are independent
    f("static", &s1);  // 'a = 'static, 'b = s1's lifetime
}

// Example: String comparison from different sources
fn compare_strings() {
    two_lifetimes(|a: &str, b: &str| a.len() > b.len());
}

HRTB with Associated Types

Combining HRTB with associated types requires careful syntax:

trait Parser {
    type Output;
    
    fn parse<'a>(&self, input: &'a [u8]) -> Option<(&'a [u8], Self::Output)>;
}

// Function accepting any parser with HRTB-like behavior
fn use_parser<P>(parser: &P)
where
    P: Parser,
    // The parse method already has HRTB-like behavior through its own 'a parameter
{
    let data = vec![1, 2, 3];
    parser.parse(&data);
    
    parser.parse(&[4, 5, 6]);  // Different lifetime
}

When to Use HRTBs

Use HRTBs when:

  1. Generic functions/closures accepting references: Your function takes a closure that will be called with references of various lifetimes

fn filter<F>(items: &[String], predicate: F) -> Vec<&str>
   where
       F: for<'a> Fn(&'a str) -> bool,

  1. Trait objects with reference parameters: Storing trait objects where methods take references

let handlers: Vec<Box<dyn for<'a> Fn(&'a Request) -> Response>>;

  1. Parser combinators: Building parsers that chain and compose

pub type Parser<T> = Box<dyn for<'a> Fn(&'a [u8]) -> ParseResult<'a, T>>;

  1. Database/transaction APIs: Operations that execute with borrowed query strings

trait Transaction {
       fn execute(&mut self, sql: &str) -> Result<Vec<Row>>;
   }

  1. Middleware/plugin systems: Handlers that process requests with borrowed data

trait Middleware: for<'a> Fn(&'a Request) -> Future<Output = Response> {}

When NOT to Use HRTBs

Avoid HRTBs when:

  1. Single lifetime suffices: If all data has the same lifetime, use a regular lifetime parameter

// DON'T use HRTB here
   fn process_batch<'a>(items: &'a [String], processor: impl Fn(&'a str)) {
       for item in items {
           processor(item);  // All items have same 'a
       }
   }

  1. Ownership is acceptable: If you can take ownership, lifetimes disappear

// Simpler: take ownership
   fn process_owned(items: Vec<String>, processor: impl Fn(String)) {
       // No lifetimes needed
   }

  1. Static data only: If you only work with 'static data

fn process_static(items: &[&'static str]) {
       // 'static is concrete, no HRTB needed
   }

  1. Adding unnecessary complexity: Don't use HRTB if you don't have multiple lifetime requirements

// DON'T over-engineer with HRTB
   fn simple<'a>(s: &'a str) -> &'a str {
       s  // Simple lifetime parameter is fine
   }

⚠️ Anti-patterns

⚠️ Anti-pattern 1: Forgetting HRTB When Needed

// WRONG: Missing HRTB causes lifetime errors
struct Validator<F>
where
    F: Fn(&str) -> bool,  // Implicit for<'a> in trait bound, but...
{
    predicate: F,
}

// ERROR: This won't compile when you use it
impl<F> Validator<F>
where
    F: Fn(&str) -> bool,
{
    fn validate_many(&self, items: &[String]) -> bool {
        // ERROR: items have different lifetime than the inferred 'a
        items.iter().all(|s| (self.predicate)(s))
    }
}

// FIXED: Explicit HRTB in where clause
struct ValidatorFixed<F>
where
    F: for<'a> Fn(&'a str) -> bool,
{
    predicate: F,
}

⚠️ Anti-pattern 2: Using HRTB When Simple Generic Suffices

// WRONG: Over-complicated with HRTB
fn process_slice<F>(items: &[String], f: F)
where
    F: for<'a> Fn(&'a str),  // Unnecessary HRTB!
{
    for item in items {
        f(item);
    }
}

// BETTER: Simple lifetime parameter
fn process_slice_simple<'a, F>(items: &'a [String], f: F)
where
    F: Fn(&'a str),  // All items have same 'a
{
    for item in items {
        f(item);
    }
}

// EVEN BETTER: Let compiler infer
fn process_slice_inferred(items: &[String], f: impl Fn(&str)) {
    for item in items {
        f(item);
    }
}

⚠️ Anti-pattern 3: Confusing for<'a> with <'a>

// WRONG: Misunderstanding the syntax
trait BadParser<'a> {  // This is NOT HRTB
    fn parse(&self, input: &'a [u8]) -> Option<&'a [u8]>;
}

// Problem: 'a is fixed for each implementation, can't parse
// multiple buffers with different lifetimes

// CORRECT: HRTB in trait method
trait GoodParser {
    fn parse<'a>(&self, input: &'a [u8]) -> Option<&'a [u8]>;
    // Each call can have different 'a
}

// Also CORRECT: HRTB in trait bound
fn use_parser<P>(parser: P)
where
    P: for<'a> Fn(&'a [u8]) -> Option<&'a [u8]>,
{
    // P can be called with any lifetime
}

⚠️ Anti-pattern 4: Mixing HRTB with Concrete Lifetimes

// WRONG: Mixing concrete and higher-ranked lifetimes
fn confused<'a, F>(data: &'a str, f: F)
where
    F: for<'b> Fn(&'a str, &'b str) -> bool,  // Confusing!
{
    // 'a is fixed but 'b is universally quantified - unusual pattern
}

// BETTER: Either make both concrete or both universal
fn better_concrete<'a, 'b, F>(data1: &'a str, data2: &'b str, f: F)
where
    F: Fn(&'a str, &'b str) -> bool,  // Both concrete
{
}

fn better_universal<F>(f: F)
where
    F: for<'a, 'b> Fn(&'a str, &'b str) -> bool,  // Both universal
{
}

Performance Characteristics

Runtime Performance

HRTB has ZERO runtime overhead:
  • HRTBs are purely a compile-time concept
  • No different than regular lifetime parameters at runtime
  • Generated machine code is identical
// These generate identical assembly
fn without_hrtb<'a>(s: &'a str) -> usize {
    s.len()
}

fn with_hrtb<F>(f: F) -> usize
where
    F: for<'a> Fn(&'a str) -> usize,
{
    f("test")
}

// Calling with_hrtb(|s| s.len()) produces same code as without_hrtb("test")

Compile-Time Impact

HRTBs can affect compilation:

  1. Type checking complexity: More complex lifetime constraints = longer type checking
  2. Error messages: HRTB errors can be harder to understand
  3. Monomorphization: Generic functions with HRTB still monomorphize normally
Tips for compilation performance:
  • Use HRTBs only where needed
  • Prefer impl Trait which implicitly adds HRTB
  • Consider trait objects if compile times are problematic (trades runtime for compile time)

Memory Layout

HRTB doesn't affect memory layout:

use std::mem::size_of_val;

let closure = |s: &str| s.len();

// Closure size is the same regardless of HRTB
fn takes_hrtb<F: for<'a> Fn(&'a str) -> usize>(f: F) {
    println!("Size: {}", size_of_val(&f));
}

fn takes_regular<'a, F: Fn(&'a str) -> usize>(f: F) {
    println!("Size: {}", size_of_val(&f));
}

takes_hrtb(closure);  // Same size
takes_regular(closure);  // Same size

Exercises

Exercise 1: Basic HRTB Closure (Beginner)

Task: Implement a function that takes a closure for validating strings, then calls it with strings from different scopes.
// TODO: Add correct HRTB bound
fn validate_strings<F>(validator: F) -> bool
where
    F: /* YOUR BOUND HERE */
{
    // Test with temporary String
    let temp = String::from("test@example.com");
    if !validator(&temp) {
        return false;
    }

    // Test with static str
    if !validator("admin@test.org") {
        return false;
    }

    // Test with another temporary
    let another = format!("user{}@domain.com", 123);
    validator(&another)
}

fn main() {
    let is_valid = validate_strings(|s: &str| {
        s.contains('@') && s.len() > 5
    });

    println!("All valid: {}", is_valid);
}
Solution:
fn validate_strings<F>(validator: F) -> bool
where
    F: for<'a> Fn(&'a str) -> bool,
{
    let temp = String::from("test@example.com");
    if !validator(&temp) {
        return false;
    }

    if !validator("admin@test.org") {
        return false;
    }

    let another = format!("user{}@domain.com", 123);
    validator(&another)
}

Exercise 2: Parser Combinator (Intermediate)

Task: Implement a simple parser combinator that parses two consecutive items using HRTB.
type ParseResult<'a, T> = Option<(&'a [u8], T)>;

// TODO: Add HRTB to make this work
trait Parser<T> {
    fn parse /* YOUR SIGNATURE HERE */;
}

// TODO: Implement a combinator that chains two parsers
fn then<P1, P2, A, B>(parser1: P1, parser2: P2) -> /* RETURN TYPE */
where
    P1: /* YOUR BOUND */,
    P2: /* YOUR BOUND */,
{
    // TODO: Return a parser that runs parser1, then parser2
    todo!()
}

fn main() {
    // Parse a byte, then another byte
    let parser = then(
        |input: &[u8]| {
            if input.is_empty() {
                None
            } else {
                Some((&input[1..], input[0]))
            }
        },
        |input: &[u8]| {
            if input.is_empty() {
                None
            } else {
                Some((&input[1..], input[0]))
            }
        },
    );

    let result = parser.parse(&[1, 2, 3]);
    println!("{:?}", result);
}
Solution:
type ParseResult<'a, T> = Option<(&'a [u8], T)>;

trait Parser<T> {
    fn parse<'a>(&self, input: &'a [u8]) -> ParseResult<'a, T>;
}

impl<F, T> Parser<T> for F
where
    F: for<'a> Fn(&'a [u8]) -> ParseResult<'a, T>,
{
    fn parse<'a>(&self, input: &'a [u8]) -> ParseResult<'a, T> {
        self(input)
    }
}

fn then<P1, P2, A, B>(parser1: P1, parser2: P2) -> impl Parser<(A, B)>
where
    P1: Parser<A>,
    P2: Parser<B>,
{
    move |input: &[u8]| {
        let (remaining, value1) = parser1.parse(input)?;
        let (remaining, value2) = parser2.parse(remaining)?;
        Some((remaining, (value1, value2)))
    }
}

Exercise 3: Async Middleware System (Advanced)

Task: Build a complete async middleware system with HRTB that supports:
  • Multiple middleware in a stack
  • Request/response transformation
  • Short-circuiting (middleware can stop the chain)
use std::future::Future;
use std::pin::Pin;

#[derive(Clone)]
struct Request<'a> {
    path: &'a str,
    headers: Vec<(&'a str, &'a str)>,
}

struct Response {
    status: u16,
    body: Vec<u8>,
}

// TODO: Define Middleware trait with HRTB
trait Middleware {
    // YOUR SIGNATURE HERE
}

// TODO: Implement middleware chain
struct MiddlewareChain {
    // YOUR FIELDS HERE
}

impl MiddlewareChain {
    fn new() -> Self {
        todo!()
    }

    fn add<M: Middleware + 'static>(&mut self, middleware: M) {
        todo!()
    }

    async fn execute<'a>(&self, req: Request<'a>) -> Response {
        todo!()
    }
}

#[tokio::main]
async fn main() {
    let mut chain = MiddlewareChain::new();

    // Add authentication middleware
    chain.add(|req: Request<'_>| async move {
        if req.path.contains("admin") {
            Response {
                status: 401,
                body: b"Unauthorized".to_vec(),
            }
        } else {
            Response {
                status: 200,
                body: b"OK".to_vec(),
            }
        }
    });

    // Test with different lifetimes
    let path1 = String::from("/api/users");
    let req1 = Request {
        path: &path1,
        headers: vec![],
    };
    let resp1 = chain.execute(req1).await;
    println!("Response 1: {}", resp1.status);

    let req2 = Request {
        path: "/admin/settings",
        headers: vec![],
    };
    let resp2 = chain.execute(req2).await;
    println!("Response 2: {}", resp2.status);
}
Solution:
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;

#[derive(Clone)]
struct Request<'a> {
    path: &'a str,
    headers: Vec<(&'a str, &'a str)>,
}

struct Response {
    status: u16,
    body: Vec<u8>,
}

trait Middleware: Send + Sync {
    fn call<'a>(
        &'a self,
        req: Request<'a>,
    ) -> Pin<Box<dyn Future<Output = Response> + Send + 'a>>;
}

impl<F, Fut> Middleware for F
where
    F: Fn(Request<'_>) -> Fut + Send + Sync,
    Fut: Future<Output = Response> + Send,
{
    fn call<'a>(
        &'a self,
        req: Request<'a>,
    ) -> Pin<Box<dyn Future<Output = Response> + Send + 'a>> {
        Box::pin(self(req))
    }
}

struct MiddlewareChain {
    middleware: Vec<Arc<dyn Middleware>>,
}

impl MiddlewareChain {
    fn new() -> Self {
        Self {
            middleware: Vec::new(),
        }
    }

    fn add<M: Middleware + 'static>(&mut self, middleware: M) {
        self.middleware.push(Arc::new(middleware));
    }

    async fn execute<'a>(&self, req: Request<'a>) -> Response {
        for m in &self.middleware {
            let resp = m.call(req.clone()).await;
            if resp.status != 200 {
                return resp; // Short-circuit on error
            }
        }
        Response {
            status: 200,
            body: b"All middleware passed".to_vec(),
        }
    }
}

Real-World Usage

HRTBs appear throughout the Rust ecosystem:

Standard Library

  1. Closure traits (std::ops):

pub trait Fn<Args>: FnMut<Args> {
       extern "rust-call" fn call(&self, args: Args) -> Self::Output;
   }
   // Implemented as: for<'a> Fn(&'a T) when used with references

  1. Iterator methods:

fn filter<P>(self, predicate: P) -> Filter<Self, P>
   where
       P: FnMut(&Self::Item) -> bool,  // Implicitly: for<'a> FnMut(&'a Item)

nom Parser Library

// From nom's IResult type
pub type IResult<I, O, E = Error<I>> = Result<(I, O), Err<E>>;

// Parser trait (simplified)
pub trait Parser<I, O, E> {
    fn parse(&mut self, input: I) -> IResult<I, O, E>;
}

// All nom combinators use HRTB patterns
pub fn tag<'a>(tag: &'a str) -> impl Fn(&'a str) -> IResult<&'a str, &'a str>;

tokio Runtime

// Spawning tasks with borrowed data
pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
where
    F: Future + Send + 'static,
    F::Output: Send + 'static,
{
    // HRTB used internally for task scheduling
}

// Block on with HRTB for local sets
pub fn block_on<F: Future>(&self, future: F) -> F::Output;

diesel Database Library

// diesel's query execution uses HRTB
trait Connection {
    fn execute(&mut self, query: &str) -> QueryResult<usize>;
    
    fn transaction<T, E, F>(&mut self, f: F) -> Result<T, E>
    where
        F: FnOnce(&mut Self) -> Result<T, E>,  // HRTB in practice
        E: From<Error>;
}

actix-web Framework

// Middleware trait (simplified)
pub trait Transform<S, Req> {
    type Response;
    type Error;
    type Transform: Service<Req, Response = Self::Response, Error = Self::Error>;

    fn new_transform(&self, service: S) -> Self::Transform;
}

// Handler functions use HRTB for request extraction
pub trait Handler<Args>: Clone + 'static {
    type Output;
    fn call(&self, args: Args) -> Self::Output;
}

Further Reading

RFCs and Official Documentation

  1. RFC 387 - Higher-Ranked Trait Bounds
  • https://rust-lang.github.io/rfcs/0387-higher-ranked-trait-bounds.html
  • The original RFC introducing HRTB syntax
  1. Rust Reference - Higher-Ranked Trait Bounds
  • https://doc.rust-lang.org/reference/trait-bounds.html#higher-ranked-trait-bounds
  • Official language reference
  1. Rustonomicon - Higher-Rank Trait Bounds
  • https://doc.rust-lang.org/nomicon/hrtb.html
  • Advanced explanation of lifetime rank

Blog Posts and Tutorials

  1. "Understanding Rust's Higher-Ranked Trait Bounds" by Yoshua Wuyts
  • Deep dive into HRTB motivation and examples
  1. "Lifetimes and HRTB" by preslav.me
  • Practical guide to when you need HRTB
  1. "The Rust Programming Language" Book - Advanced Lifetimes
  • https://doc.rust-lang.org/book/ch19-02-advanced-lifetimes.html

Library Documentation

  1. nom parser combinators
  • https://docs.rs/nom/
  • Extensive HRTB usage examples
  1. tokio async runtime
  • https://docs.rs/tokio/
  • Async HRTB patterns
  1. diesel ORM
  • https://docs.rs/diesel/
  • Database transaction HRTB patterns

Academic Papers

  1. "System F-omega with Higher-Ranked Types"
  • Theoretical foundation for higher-ranked polymorphism
  1. "Practical Type Inference for Arbitrary-Rank Types"
  • How compilers infer higher-ranked types

---

Next Steps:
  • Practice implementing parser combinators with HRTB
  • Study closure trait implementations in the standard library
  • Build async middleware systems using HRTB patterns
  • Explore diesel and nom source code for real-world HRTB usage
Common Gotchas:
  • Forgetting for<'a> in where clauses (compiler won't always infer)
  • Mixing concrete and higher-ranked lifetimes
  • Over-using HRTB when simple lifetime parameters work
  • Not understanding when HRTB is implicit vs explicit
Remember: HRTBs are powerful but subtle. They solve real problems when you need generic code that works with references of multiple lifetimes. Start with simple examples and gradually build to complex patterns like parser combinators and async middleware.

🎮 Try it Yourself

🎮

Higher-Ranked Trait Bounds - Playground

Run this code in the official Rust Playground