Rental Pattern
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
Rental owns a String and provides &str views via rent(&self) -> &strParsedRental stores raw data and indices derived from it, computing views on demand&str from rent is tied to self's lifetime (cannot outlive the rental)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
rent(&self) -> &str cannot outlive self; OCaml's GC achieves the same guarantee dynamically.Vec<usize> indices, computing &str views on demand — a common pattern to avoid self-reference; OCaml stores lazy Lazy.t computations.ouroboros, self_cell, and yoke provide macro-generated safe rental APIs for complex cases; OCaml has no equivalent because the problem does not exist.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"]);
}
}#[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
Exercises
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.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.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>.