ExamplesBy LevelBy TopicLearning Paths
556 Intermediate

Rental Pattern

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Rental Pattern" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. The rental pattern addresses a common need: a type that owns its data and provides borrowing access to it — "renting out" references into its own storage. Key difference from OCaml: 1. **Lifetime enforcement**: Rust's type system ensures `rent(&self)

Tutorial

The Problem

The rental pattern addresses a common need: a type that owns its data and provides borrowing access to it — "renting out" references into its own storage. This is a structured version of the owning-reference problem. The rental crate (now deprecated) automated this with macros; the ouroboros and self_cell crates provide safe modern alternatives. Understanding the manual implementation helps explain why the borrow checker prevents naive self-referential structs and what makes the pattern sound.

🎯 Learning Outcomes

  • • How Rental owns a String and provides &str views via rent(&self) -> &str
  • • How ParsedRental stores raw data and indices derived from it, computing views on demand
  • • Why the returned &str from rent is tied to self's lifetime (cannot outlive the rental)
  • • How lazy parsing separates expensive work from construction
  • • Where rental appears: HTTP request parsing, JSON tree traversal, configuration loading
  • Code Example

    #![allow(clippy::all)]
    //! Rental Pattern
    //!
    //! Owning data and borrowing from it simultaneously.
    
    /// Simple rental: owns data and provides view.
    pub struct Rental {
        data: String,
    }
    
    impl Rental {
        pub fn new(data: &str) -> Self {
            Rental {
                data: data.to_string(),
            }
        }
    
        pub fn rent(&self) -> &str {
            &self.data
        }
    
        pub fn rent_slice(&self, start: usize, end: usize) -> &str {
            &self.data[start..end.min(self.data.len())]
        }
    }
    
    /// Rental with lazy parsing.
    pub struct ParsedRental {
        raw: String,
        parsed: Vec<usize>, // indices into raw
    }
    
    impl ParsedRental {
        pub fn new(raw: &str) -> Self {
            let parsed = raw
                .char_indices()
                .filter(|(_, c)| c.is_whitespace())
                .map(|(i, _)| i)
                .collect();
            ParsedRental {
                raw: raw.to_string(),
                parsed,
            }
        }
    
        pub fn words(&self) -> Vec<&str> {
            let mut words = Vec::new();
            let mut start = 0;
            for &end in &self.parsed {
                if start < end {
                    words.push(&self.raw[start..end]);
                }
                start = end + 1;
            }
            if start < self.raw.len() {
                words.push(&self.raw[start..]);
            }
            words
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_rental() {
            let r = Rental::new("hello world");
            assert_eq!(r.rent(), "hello world");
            assert_eq!(r.rent_slice(0, 5), "hello");
        }
    
        #[test]
        fn test_parsed_rental() {
            let r = ParsedRental::new("hello world rust");
            let words = r.words();
            assert_eq!(words, vec!["hello", "world", "rust"]);
        }
    }

    Key Differences

  • Lifetime enforcement: Rust's type system ensures rent(&self) -> &str cannot outlive self; OCaml's GC achieves the same guarantee dynamically.
  • Lazy parse: Rust's lazy parse stores Vec<usize> indices, computing &str views on demand — a common pattern to avoid self-reference; OCaml stores lazy Lazy.t computations.
  • Crate ecosystem: ouroboros, self_cell, and yoke provide macro-generated safe rental APIs for complex cases; OCaml has no equivalent because the problem does not exist.
  • Slice copying: rent_slice in OCaml (String.sub) copies the data; Rust &str slices are zero-copy views into the owned String.
  • OCaml Approach

    OCaml makes the rental pattern trivial — a record holding a string and methods returning slices of it are straightforward:

    type rental = { raw: string; mutable parsed: int list }
    let rent r = r.raw
    let rent_slice r s e = String.sub r.raw s (e - s)  (* copies *)
    

    The GC ensures the raw string stays alive as long as any view exists.

    Full Source

    #![allow(clippy::all)]
    //! Rental Pattern
    //!
    //! Owning data and borrowing from it simultaneously.
    
    /// Simple rental: owns data and provides view.
    pub struct Rental {
        data: String,
    }
    
    impl Rental {
        pub fn new(data: &str) -> Self {
            Rental {
                data: data.to_string(),
            }
        }
    
        pub fn rent(&self) -> &str {
            &self.data
        }
    
        pub fn rent_slice(&self, start: usize, end: usize) -> &str {
            &self.data[start..end.min(self.data.len())]
        }
    }
    
    /// Rental with lazy parsing.
    pub struct ParsedRental {
        raw: String,
        parsed: Vec<usize>, // indices into raw
    }
    
    impl ParsedRental {
        pub fn new(raw: &str) -> Self {
            let parsed = raw
                .char_indices()
                .filter(|(_, c)| c.is_whitespace())
                .map(|(i, _)| i)
                .collect();
            ParsedRental {
                raw: raw.to_string(),
                parsed,
            }
        }
    
        pub fn words(&self) -> Vec<&str> {
            let mut words = Vec::new();
            let mut start = 0;
            for &end in &self.parsed {
                if start < end {
                    words.push(&self.raw[start..end]);
                }
                start = end + 1;
            }
            if start < self.raw.len() {
                words.push(&self.raw[start..]);
            }
            words
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_rental() {
            let r = Rental::new("hello world");
            assert_eq!(r.rent(), "hello world");
            assert_eq!(r.rent_slice(0, 5), "hello");
        }
    
        #[test]
        fn test_parsed_rental() {
            let r = ParsedRental::new("hello world rust");
            let words = r.words();
            assert_eq!(words, vec!["hello", "world", "rust"]);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_rental() {
            let r = Rental::new("hello world");
            assert_eq!(r.rent(), "hello world");
            assert_eq!(r.rent_slice(0, 5), "hello");
        }
    
        #[test]
        fn test_parsed_rental() {
            let r = ParsedRental::new("hello world rust");
            let words = r.words();
            assert_eq!(words, vec!["hello", "world", "rust"]);
        }
    }

    Deep Comparison

    OCaml vs Rust: lifetime rental pattern

    See example.rs and example.ml for implementations.

    Key Differences

  • OCaml uses garbage collection
  • Rust uses ownership and borrowing
  • Both support the core concept
  • Exercises

  • CSV rental: Implement struct CsvRental { raw: String, row_offsets: Vec<(usize, usize)> } where row_offsets stores (start, end) pairs; row(&self, n: usize) -> &str returns a zero-copy view.
  • Lazy parsed rental: Add a fields(&self, row: usize) -> Vec<&str> method to CsvRental that splits the row on commas and returns field slices — all zero-copy from the owned String.
  • Parse-on-demand: Implement a Config struct that stores a raw String and parses it into a HashMap<String, String> lazily using OnceLock, providing get(&self, key: &str) -> Option<&str>.
  • Open Source Repos