Lifetimes in impl Blocks
Tutorial Video
Text description (accessibility)
This video demonstrates the "Lifetimes in impl Blocks" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. When a struct has a lifetime parameter, every `impl` block for that struct must repeat the lifetime parameter and can use it in method signatures. Key difference from OCaml: 1. **Return lifetime source**: Rust methods must distinguish whether a returned reference comes from `self` or from the stored `'a` data — different lifetimes with different scopes; OCaml has no such distinction.
Tutorial
The Problem
When a struct has a lifetime parameter, every impl block for that struct must repeat the lifetime parameter and can use it in method signatures. The critical subtlety is that methods can return references with either the struct's lifetime ('a) or the lifetime of &self — and these are different. A method returning &'a T can return data that outlives the method call; a method returning &T tied to &self is only valid for the duration of the method borrow. Understanding this distinction prevents common confusion when implementing view-type APIs.
🎯 Learning Outcomes
impl<'a, T> View<'a, T> propagates the struct's lifetime into method signaturesfn get(&self, index: usize) -> Option<&'a T> returns data with the stored lifetime, not self'sfn slice(&self, ...) -> Option<View<'a, T>> creates a sub-view with the same parent lifetime&'a T vs &T (where T is tied to self)Code Example
pub struct View<'a, T> {
data: &'a [T],
}
// 'a appears on impl and is used in methods
impl<'a, T> View<'a, T> {
pub fn new(data: &'a [T]) -> Self { View { data } }
// Return type tied to 'a, not &self
pub fn get(&self, index: usize) -> Option<&'a T> {
self.data.get(index)
}
}Key Differences
self or from the stored 'a data — different lifetimes with different scopes; OCaml has no such distinction.slice returns View<'a, T> with the same lifetime as the original — no copy made; OCaml slice creates a new record pointing into the same array, relying on GC for safety.impl<'a, T> methods often repeat 'a in return types; OCaml methods on parameterized types ('a view) are simpler to write.View<'a, T> statically prevents accessing data after the source slice is freed; OCaml's GC prevents it dynamically — the array is kept alive as long as any view references it.OCaml Approach
OCaml modules implementing view-like abstractions use plain records and return values without lifetime annotations. The GC ensures the referenced data remains alive:
type 'a view = { data: 'a array; start: int; len: int }
let get v i = if i < v.len then Some v.data.(v.start + i) else None
let slice v s e = if s <= e && e <= v.len then Some { v with start = v.start + s; len = e - s } else None
Full Source
#![allow(clippy::all)]
//! Lifetimes in impl Blocks
//!
//! Lifetime annotations in impl blocks for structs with borrowed data.
/// A slice view — borrows from an underlying slice.
pub struct View<'a, T> {
data: &'a [T],
}
impl<'a, T> View<'a, T> {
/// Constructor: same 'a lifetime.
pub fn new(data: &'a [T]) -> Self {
View { data }
}
/// get: returns reference tied to 'a (the data's lifetime).
pub fn get(&self, index: usize) -> Option<&'a T> {
self.data.get(index)
}
/// Returns a sub-view with the same lifetime 'a.
pub fn slice(&self, start: usize, end: usize) -> Option<View<'a, T>> {
if start <= end && end <= self.data.len() {
Some(View {
data: &self.data[start..end],
})
} else {
None
}
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &'a T> {
self.data.iter()
}
}
/// Buffer with reader — different lifetime patterns.
pub struct Buffer<'a> {
content: &'a str,
position: usize,
}
impl<'a> Buffer<'a> {
pub fn new(content: &'a str) -> Self {
Buffer {
content,
position: 0,
}
}
/// Read n chars, return slice tied to 'a.
pub fn read(&mut self, n: usize) -> &'a str {
let end = (self.position + n).min(self.content.len());
let result = &self.content[self.position..end];
self.position = end;
result
}
pub fn remaining(&self) -> &'a str {
&self.content[self.position..]
}
pub fn position(&self) -> usize {
self.position
}
}
/// Generic container with lifetime.
pub struct Container<'a, T> {
items: Vec<&'a T>,
}
impl<'a, T> Container<'a, T> {
pub fn new() -> Self {
Container { items: Vec::new() }
}
pub fn add(&mut self, item: &'a T) {
self.items.push(item);
}
pub fn get(&self, index: usize) -> Option<&'a T> {
self.items.get(index).copied()
}
}
impl<'a, T> Default for Container<'a, T> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_view_basic() {
let data = [1, 2, 3, 4, 5];
let view = View::new(&data);
assert_eq!(view.get(2), Some(&3));
assert_eq!(view.len(), 5);
}
#[test]
fn test_view_slice() {
let data = [1, 2, 3, 4, 5];
let view = View::new(&data);
let sub = view.slice(1, 4).unwrap();
assert_eq!(sub.len(), 3);
assert_eq!(sub.get(0), Some(&2));
}
#[test]
fn test_view_iter() {
let data = [1, 2, 3];
let view = View::new(&data);
let sum: i32 = view.iter().sum();
assert_eq!(sum, 6);
}
#[test]
fn test_buffer_read() {
let content = "Hello, World!";
let mut buffer = Buffer::new(content);
assert_eq!(buffer.read(5), "Hello");
assert_eq!(buffer.read(2), ", ");
assert_eq!(buffer.remaining(), "World!");
}
#[test]
fn test_container() {
let a = 1;
let b = 2;
let c = 3;
let mut container: Container<i32> = Container::new();
container.add(&a);
container.add(&b);
container.add(&c);
assert_eq!(container.get(1), Some(&2));
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_view_basic() {
let data = [1, 2, 3, 4, 5];
let view = View::new(&data);
assert_eq!(view.get(2), Some(&3));
assert_eq!(view.len(), 5);
}
#[test]
fn test_view_slice() {
let data = [1, 2, 3, 4, 5];
let view = View::new(&data);
let sub = view.slice(1, 4).unwrap();
assert_eq!(sub.len(), 3);
assert_eq!(sub.get(0), Some(&2));
}
#[test]
fn test_view_iter() {
let data = [1, 2, 3];
let view = View::new(&data);
let sum: i32 = view.iter().sum();
assert_eq!(sum, 6);
}
#[test]
fn test_buffer_read() {
let content = "Hello, World!";
let mut buffer = Buffer::new(content);
assert_eq!(buffer.read(5), "Hello");
assert_eq!(buffer.read(2), ", ");
assert_eq!(buffer.remaining(), "World!");
}
#[test]
fn test_container() {
let a = 1;
let b = 2;
let c = 3;
let mut container: Container<i32> = Container::new();
container.add(&a);
container.add(&b);
container.add(&c);
assert_eq!(container.get(1), Some(&2));
}
}
Deep Comparison
OCaml vs Rust: Lifetimes in impl Blocks
OCaml
(* Methods just work — no lifetime management *)
type 'a view = { data: 'a array }
let make_view data = { data }
let get view i = view.data.(i)
let slice view start end_ =
{ data = Array.sub view.data start (end_ - start) }
Rust
pub struct View<'a, T> {
data: &'a [T],
}
// 'a appears on impl and is used in methods
impl<'a, T> View<'a, T> {
pub fn new(data: &'a [T]) -> Self { View { data } }
// Return type tied to 'a, not &self
pub fn get(&self, index: usize) -> Option<&'a T> {
self.data.get(index)
}
}
Key Differences
Exercises
struct ViewMut<'a, T> { data: &'a mut [T] } with fn get_mut(&mut self, i: usize) -> Option<&mut T> — note the lifetime difference from the immutable version.fn chunks(&self, size: usize) -> Vec<View<'a, T>> that splits the view into equal-sized sub-views without copying data.struct ViewIter<'a, T> { view: &'a View<'a, T>, pos: usize } as an Iterator<Item = &'a T> that yields elements from the view.