ExamplesBy LevelBy TopicLearning Paths
542 Intermediate

Higher-Ranked Trait Bounds (for<'a>)

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Higher-Ranked Trait Bounds (for<'a>)" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Standard lifetime parameters on functions are monomorphic: `F: Fn(&'a str) -> &'a str` means `F` works for one specific lifetime `'a` chosen by the caller. Key difference from OCaml: 1. **Implicit vs explicit**: In Rust, `Fn(&T)

Tutorial

The Problem

Standard lifetime parameters on functions are monomorphic: F: Fn(&'a str) -> &'a str means F works for one specific lifetime 'a chosen by the caller. But some abstractions need functions that work for any lifetime — a callback that processes strings of any duration, not just one specific scope. Higher-Ranked Trait Bounds (HRTB) with for<'a> express universal quantification over lifetimes: F: for<'a> Fn(&'a str) -> &'a str means F must work for every possible lifetime. This is essential for trait objects, parser combinators, and middleware that receive arbitrarily-scoped references.

🎯 Learning Outcomes

  • • What for<'a> means: the bound must hold for all lifetimes simultaneously
  • • How apply_hrtb<F: for<'a> Fn(&'a str) -> &'a str> differs from a fixed-lifetime version
  • • How HRTBs appear implicitly in common Rust patterns like F: Fn(&T) -> &T
  • • How to write traits whose methods have lifetime parameters (trait Processor)
  • • Where HRTBs are essential: trait objects storing callbacks, Iterator::for_each, serde deserializers
  • Code Example

    // HRTB: for<'a> quantifies over all lifetimes
    pub fn apply_hrtb<F>(f: F, s: &str) -> String
    where
        F: for<'a> Fn(&'a str) -> &'a str,
    {
        f(s).to_string()
    }
    
    // Common use: callbacks that work on any borrow
    fn transform<F>(f: F) where F: for<'a> Fn(&'a T) -> &'a T

    Key Differences

  • Implicit vs explicit: In Rust, Fn(&T) -> &T implicitly introduces for<'a> — it is commonly written without explicit for<'a>; the syntax only appears when necessary for clarity.
  • Rank-2 types: OCaml needs record wrapping for rank-2 polymorphic functions (functions that take polymorphic functions); Rust's for<'a> achieves this for lifetime polymorphism.
  • Common implicit HRTB: Many Rust programs use HRTBs without knowing it — impl Fn(&str) implicitly means impl for<'a> Fn(&'a str).
  • Error messages: HRTB errors in Rust can be cryptic — the compiler reports lifetime bound violations that reference for<'a> without explaining it well.
  • OCaml Approach

    OCaml's HM type system achieves similar genericity through polymorphism. A function that processes strings of any kind is simply:

    let apply f s = f s   (* works for any 'a -> 'b *)
    let transform_all f items = List.map f items
    

    OCaml's rank-2 polymorphism (for functions that must be polymorphic in their arguments) requires explicit type annotations with forall using the module system or record wrapping.

    Full Source

    #![allow(clippy::all)]
    //! Higher-Ranked Trait Bounds (for<'a>)
    //!
    //! Universal quantification over lifetimes for flexible callbacks.
    
    /// Without HRTB: lifetime fixed at call site.
    pub fn apply_fixed<'a, F>(f: F, s: &'a str) -> &'a str
    where
        F: Fn(&'a str) -> &'a str,
    {
        f(s)
    }
    
    /// With HRTB: F works for ANY lifetime.
    pub fn apply_hrtb<F>(f: F, s: &str) -> String
    where
        F: for<'a> Fn(&'a str) -> &'a str,
    {
        f(s).to_string()
    }
    
    /// HRTB in trait definitions.
    pub trait Processor {
        fn process<'a>(&self, input: &'a str) -> &'a str;
    }
    
    /// Common HRTB pattern: Fn(&T) -> &T.
    pub fn transform_all<F>(items: &[String], f: F) -> Vec<String>
    where
        F: for<'a> Fn(&'a str) -> &'a str,
    {
        items.iter().map(|s| f(s).to_string()).collect()
    }
    
    /// Identity processor (for<'a> Fn(&'a str) -> &'a str).
    pub fn identity(s: &str) -> &str {
        s
    }
    
    /// Trim processor.
    pub fn trim(s: &str) -> &str {
        s.trim()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_apply_fixed() {
            let s = String::from("  hello  ");
            let result = apply_fixed(|x| x.trim(), &s);
            assert_eq!(result, "hello");
        }
    
        #[test]
        fn test_apply_hrtb() {
            let s = "  world  ";
            let result = apply_hrtb(|x| x.trim(), s);
            assert_eq!(result, "world");
        }
    
        #[test]
        fn test_transform_all() {
            let items = vec![String::from("  a  "), String::from("  b  ")];
            let result = transform_all(&items, trim);
            assert_eq!(result, vec!["a", "b"]);
        }
    
        #[test]
        fn test_identity() {
            let s = "hello";
            assert_eq!(identity(s), "hello");
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_apply_fixed() {
            let s = String::from("  hello  ");
            let result = apply_fixed(|x| x.trim(), &s);
            assert_eq!(result, "hello");
        }
    
        #[test]
        fn test_apply_hrtb() {
            let s = "  world  ";
            let result = apply_hrtb(|x| x.trim(), s);
            assert_eq!(result, "world");
        }
    
        #[test]
        fn test_transform_all() {
            let items = vec![String::from("  a  "), String::from("  b  ")];
            let result = transform_all(&items, trim);
            assert_eq!(result, vec!["a", "b"]);
        }
    
        #[test]
        fn test_identity() {
            let s = "hello";
            assert_eq!(identity(s), "hello");
        }
    }

    Deep Comparison

    OCaml vs Rust: Higher-Ranked Types

    OCaml

    (* Rank-2 polymorphism via records *)
    type 'a processor = { process: 'b. 'b -> 'a }
    
    (* Or via first-class modules *)
    let apply (type a) (f : a -> a) (x : a) = f x
    

    Rust

    // HRTB: for<'a> quantifies over all lifetimes
    pub fn apply_hrtb<F>(f: F, s: &str) -> String
    where
        F: for<'a> Fn(&'a str) -> &'a str,
    {
        f(s).to_string()
    }
    
    // Common use: callbacks that work on any borrow
    fn transform<F>(f: F) where F: for<'a> Fn(&'a T) -> &'a T
    

    Key Differences

  • OCaml: Rank-2 via records or modules
  • Rust: for<'a> syntax for lifetime universality
  • Rust: HRTB common for Fn traits with references
  • Both: Enable maximally flexible callbacks
  • Rust: Compiler infers HRTB in many cases
  • Exercises

  • HRTB callback: Write a function fn apply_twice<F: for<'a> Fn(&'a str) -> &'a str>(f: F, s: &str) -> String that applies f to s twice, returning the result of the second application.
  • Trait with HRTB: Implement the Processor trait for a TrimProcessor struct and use it in transform_all — verify it works with strings of any lifetime.
  • Box<dyn ...> HRTB: Store a Box<dyn for<'a> Fn(&'a str) -> &'a str> in a struct and call it with references of different lifetimes — show the boxed closure satisfies the HRTB.
  • Open Source Repos