ExamplesBy LevelBy TopicLearning Paths
534 Intermediate

Lifetimes in impl Blocks

Functional Programming

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

  • • How impl<'a, T> View<'a, T> propagates the struct's lifetime into method signatures
  • • Why fn get(&self, index: usize) -> Option<&'a T> returns data with the stored lifetime, not self's
  • • How fn slice(&self, ...) -> Option<View<'a, T>> creates a sub-view with the same parent lifetime
  • • The difference between returning &'a T vs &T (where T is tied to self)
  • • Where this pattern is used: slice wrappers, buffer views, database row references
  • 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

  • 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.
  • Sub-view lifetime: Rust 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.
  • Method signature verbosity: Rust impl<'a, T> methods often repeat 'a in return types; OCaml methods on parameterized types ('a view) are simpler to write.
  • Safety model: Rust's 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));
        }
    }
    ✓ Tests Rust test suite
    #[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

  • OCaml: Type parameter 'a is just a generic, not lifetime
  • Rust: impl<'a, T> declares lifetime for all methods
  • Rust: Return &'a T means "valid as long as original data"
  • Rust: &self vs 'a distinction matters for return types
  • Both: Methods can return references into borrowed data
  • Exercises

  • Mutable view: Implement 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.
  • Chained slices: Write a method fn chunks(&self, size: usize) -> Vec<View<'a, T>> that splits the view into equal-sized sub-views without copying data.
  • View iterator: Implement struct ViewIter<'a, T> { view: &'a View<'a, T>, pos: usize } as an Iterator<Item = &'a T> that yields elements from the view.
  • Open Source Repos