ExamplesBy LevelBy TopicLearning Paths
537 Intermediate

Lifetime Coercion and Subtyping

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Lifetime Coercion and Subtyping" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Lifetime subtyping is the mechanism that makes Rust's borrow checker flexible without requiring programmers to always use exactly matching lifetimes. Key difference from OCaml: 1. **Automatic coercion**: Rust automatically coerces `'long` to `'short` at assignment; OCaml has no lifetime coercion because lifetimes do not exist.

Tutorial

The Problem

Lifetime subtyping is the mechanism that makes Rust's borrow checker flexible without requiring programmers to always use exactly matching lifetimes. The rule is simple: a longer lifetime can always be used where a shorter one is expected, because a reference valid for longer is certainly valid for shorter. This is similar to how a subtype can be used where its supertype is expected. Without this coercion, every reference would require perfectly matched lifetime scopes, making APIs rigid and hard to use.

🎯 Learning Outcomes

  • • Why 'static references can be passed to functions expecting shorter-lived references
  • • How 'long: 'short (outlives) expresses that 'long is a subtype of 'short
  • • How reborrowing creates a shorter-lived reference from a longer-lived one
  • • Why lifetime coercion makes it possible to store 'static items in Vec<&'short str>
  • • How demonstrate_variance<'long: 'short, 'short> expresses the outlives constraint
  • Code Example

    // Longer lifetime coerces to shorter
    pub fn use_briefly<'short>(s: &'short str) -> usize {
        s.len()
    }
    
    // 'static (longer) can be used where 'short expected
    fn demo() {
        let static_str: &'static str = "forever";
        use_briefly(static_str);  // 'static -> 'short coercion
    }
    
    // Explicit bound: 'long outlives 'short
    fn variance<'long: 'short, 'short>(r: &'long str) -> &'short str {
        r  // covariant: longer to shorter
    }

    Key Differences

  • Automatic coercion: Rust automatically coerces 'long to 'short at assignment; OCaml has no lifetime coercion because lifetimes do not exist.
  • Explicit outlives: Rust's 'long: 'short syntax expresses a compile-time constraint; OCaml programs never write or verify such relationships.
  • Subtype direction: In Rust, longer lifetimes are subtypes of shorter ones (counterintuitive but correct); OCaml subtyping is based on structural compatibility, not lifetime ordering.
  • Practical impact: Lifetime coercion enables functions with short-lived references to accept static data without special handling; OCaml APIs need no such accommodation.
  • OCaml Approach

    OCaml has no lifetime coercion because there are no lifetime annotations. The GC ensures all referenced values are kept alive. Subtyping in OCaml is structural (via polymorphic variants and object types), not lifetime-based.

    (* No equivalent concept — all references are GC-managed *)
    let use_briefly s = String.length s
    let _ = use_briefly "static string"  (* always fine *)
    

    Full Source

    #![allow(clippy::all)]
    //! Lifetime Coercion and Subtyping
    //!
    //! Longer lifetimes can be used where shorter ones are required.
    
    /// Accepts a reference valid for at least 'short duration.
    pub fn use_briefly<'short>(s: &'short str) -> usize {
        s.len()
    }
    
    /// Demonstrate implicit coercion: longer lifetime used as shorter.
    pub fn coercion_demo() -> usize {
        // 'static is longer than any local lifetime
        let static_str: &'static str = "I live forever";
    
        // 'static coerces to 'short when passed to use_briefly
        use_briefly(static_str)
    }
    
    /// Store in a Vec with shorter lifetime requirement.
    pub fn store_with_coercion<'short>(storage: &mut Vec<&'short str>, item: &'static str) {
        // 'static can be stored where 'short is expected
        storage.push(item);
    }
    
    /// Function demonstrating variance.
    pub fn demonstrate_variance<'long: 'short, 'short>(
        long_ref: &'long str,
        _short_ref: &'short str,
    ) -> &'short str {
        // 'long can be used as 'short (covariance)
        long_ref
    }
    
    /// Reborrowing: creating a shorter borrow from a longer one.
    pub fn reborrow_demo() {
        let owned = String::from("hello");
        let long_borrow: &str = &owned;
    
        // Reborrow with shorter lifetime
        let short_borrow: &str = long_borrow;
        assert_eq!(short_borrow, "hello");
    }
    
    /// Reference to reference coercion.
    pub fn ref_ref_coercion<'a, 'b: 'a>(r: &'a &'b str) -> &'a str {
        *r
    }
    
    /// Struct that accepts shorter lifetime.
    pub struct Holder<'a> {
        pub data: &'a str,
    }
    
    impl<'a> Holder<'a> {
        /// Can accept 'static (longer) for 'a.
        pub fn from_static(s: &'static str) -> Holder<'static> {
            Holder { data: s }
        }
    
        /// General constructor.
        pub fn new(data: &'a str) -> Self {
            Holder { data }
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_static_to_short() {
            let result = coercion_demo();
            assert_eq!(result, 14); // "I live forever".len()
        }
    
        #[test]
        fn test_store_with_coercion() {
            let owned = String::from("local");
            let mut storage: Vec<&str> = vec![&owned];
            store_with_coercion(&mut storage, "static");
            assert_eq!(storage.len(), 2);
        }
    
        #[test]
        fn test_demonstrate_variance() {
            let long = String::from("long lived");
            {
                let short = String::from("short");
                let result = demonstrate_variance(&long, &short);
                assert_eq!(result, "long lived");
            }
        }
    
        #[test]
        fn test_reborrow() {
            let owned = String::from("test");
            let r1: &str = &owned;
            let r2: &str = r1; // reborrow
            assert_eq!(r1, r2);
        }
    
        #[test]
        fn test_ref_ref() {
            let s = "hello";
            let r: &str = &s;
            let result = ref_ref_coercion(&r);
            assert_eq!(result, "hello");
        }
    
        #[test]
        fn test_holder_from_static() {
            let holder = Holder::from_static("static string");
            assert_eq!(holder.data, "static string");
        }
    
        #[test]
        fn test_holder_from_local() {
            let local = String::from("local");
            let holder = Holder::new(&local);
            assert_eq!(holder.data, "local");
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_static_to_short() {
            let result = coercion_demo();
            assert_eq!(result, 14); // "I live forever".len()
        }
    
        #[test]
        fn test_store_with_coercion() {
            let owned = String::from("local");
            let mut storage: Vec<&str> = vec![&owned];
            store_with_coercion(&mut storage, "static");
            assert_eq!(storage.len(), 2);
        }
    
        #[test]
        fn test_demonstrate_variance() {
            let long = String::from("long lived");
            {
                let short = String::from("short");
                let result = demonstrate_variance(&long, &short);
                assert_eq!(result, "long lived");
            }
        }
    
        #[test]
        fn test_reborrow() {
            let owned = String::from("test");
            let r1: &str = &owned;
            let r2: &str = r1; // reborrow
            assert_eq!(r1, r2);
        }
    
        #[test]
        fn test_ref_ref() {
            let s = "hello";
            let r: &str = &s;
            let result = ref_ref_coercion(&r);
            assert_eq!(result, "hello");
        }
    
        #[test]
        fn test_holder_from_static() {
            let holder = Holder::from_static("static string");
            assert_eq!(holder.data, "static string");
        }
    
        #[test]
        fn test_holder_from_local() {
            let local = String::from("local");
            let holder = Holder::new(&local);
            assert_eq!(holder.data, "local");
        }
    }

    Deep Comparison

    OCaml vs Rust: Lifetime Coercion

    OCaml

    (* No lifetime coercion concept — GC manages all *)
    let use_briefly s = String.length s
    
    let demo () =
      let long_lived = "static string" in
      use_briefly long_lived
    

    Rust

    // Longer lifetime coerces to shorter
    pub fn use_briefly<'short>(s: &'short str) -> usize {
        s.len()
    }
    
    // 'static (longer) can be used where 'short expected
    fn demo() {
        let static_str: &'static str = "forever";
        use_briefly(static_str);  // 'static -> 'short coercion
    }
    
    // Explicit bound: 'long outlives 'short
    fn variance<'long: 'short, 'short>(r: &'long str) -> &'short str {
        r  // covariant: longer to shorter
    }
    

    Key Differences

  • OCaml: No explicit lifetime relationships
  • Rust: 'long: 'short means 'long outlives 'short
  • Rust: Longer lifetimes coerce to shorter automatically
  • Rust: Enables 'static to be used anywhere
  • Rust: Variance rules determine valid coercions
  • Exercises

  • Longest-lifetime function: Write fn most_general<'a>(s: &'a str) -> &'a str { s } and show it can accept both &'static str and short-lived references.
  • Vec of mixed lifetimes: Build a Vec<&str> and insert both &'static str literals and references to local String values — verify the compiler correctly constrains the vec's lifetime.
  • Subtyping chain: Write three functions with lifetimes 'a: 'b: 'c where the first returns &'a str, the second accepts &'b str, and the third accepts &'c str — show the chain compiles.
  • Open Source Repos