ExamplesBy LevelBy TopicLearning Paths
103 Intermediate

103-borrowing-shared — Shared Borrowing

Functional Programming

Tutorial

The Problem

Safe concurrent read access is the foundation of parallel computing. In C, multiple threads reading the same data simultaneously is fine as long as no thread writes, but the language does not enforce this rule — a race condition occurs silently. Rust's borrow checker enforces the reader-writer invariant at compile time: any number of shared references (&T) can coexist, but no mutable reference (&mut T) can coexist with any other reference.

This is Rust's implementation of the "readers-writers" lock at zero runtime cost — the type system acts as the lock.

🎯 Learning Outcomes

  • • Understand Rust's shared borrow rule: multiple &T references can coexist
  • • Pass &T to functions without transferring ownership
  • • Hold multiple shared references to the same value simultaneously
  • • Understand why shared references guarantee the data will not change
  • • Contrast shared borrowing with mutable borrowing and ownership
  • Code Example

    #![allow(clippy::all)]
    // 103: Shared Borrowing — &T
    // Multiple readers, no writers
    
    fn sum(data: &[i32]) -> i32 {
        data.iter().sum()
    }
    
    fn count(data: &[i32]) -> usize {
        data.len()
    }
    
    fn average(data: &[i32]) -> f64 {
        // Multiple shared borrows simultaneously — perfectly safe
        let s = sum(data); // &data borrow 1
        let c = count(data); // &data borrow 2 — fine!
        s as f64 / c as f64
    }
    
    fn first_and_last(data: &[i32]) -> Option<(i32, i32)> {
        if data.is_empty() {
            None
        } else {
            Some((data[0], data[data.len() - 1]))
        }
    }
    
    // Multiple shared references can coexist
    fn demonstrate_multiple_borrows() {
        let data = vec![1, 2, 3, 4, 5];
        let r1 = &data;
        let r2 = &data;
        let r3 = &data;
        // All three references valid simultaneously
        println!("r1={:?}, r2={:?}, r3={:?}", r1[0], r2[1], r3[2]);
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_average() {
            assert!((average(&[1, 2, 3, 4, 5]) - 3.0).abs() < 0.001);
        }
    
        #[test]
        fn test_first_and_last() {
            assert_eq!(first_and_last(&[10, 20, 30]), Some((10, 30)));
            assert_eq!(first_and_last(&[]), None);
        }
    
        #[test]
        fn test_multiple_borrows() {
            let v = vec![1, 2, 3];
            let r1 = &v;
            let r2 = &v;
            assert_eq!(r1.len(), r2.len());
        }
    }

    Key Differences

  • Compile-time enforcement: Rust enforces the readers-writers rule at compile time; OCaml relies on the programmer to avoid races in concurrent code.
  • Zero runtime cost: Rust's shared references are raw pointers at the machine level; OCaml's references go through GC indirection.
  • Concurrent reads: Rust guarantees that a &T reference means the data cannot change — no locking needed for read-only parallel access; OCaml requires explicit synchronisation for safe concurrent mutation.
  • Lifetime tracking: Rust's compiler tracks how long each borrow lives to prevent dangling pointers; OCaml's GC prevents dangling references at runtime.
  • OCaml Approach

    OCaml has no borrow checking. Any binding can read any value at any time because the GC manages all values:

    let data = [1; 2; 3; 4; 5]
    let s = List.fold_left (+) 0 data  (* data still accessible *)
    let n = List.length data            (* data still accessible *)
    let avg = float_of_int s /. float_of_int n
    

    Shared reading is always safe in OCaml because the GC ensures the data lives as long as any reference exists. There is no way to express or enforce mutation exclusivity at the type level (without external libraries like Base.Ref with locking).

    Full Source

    #![allow(clippy::all)]
    // 103: Shared Borrowing — &T
    // Multiple readers, no writers
    
    fn sum(data: &[i32]) -> i32 {
        data.iter().sum()
    }
    
    fn count(data: &[i32]) -> usize {
        data.len()
    }
    
    fn average(data: &[i32]) -> f64 {
        // Multiple shared borrows simultaneously — perfectly safe
        let s = sum(data); // &data borrow 1
        let c = count(data); // &data borrow 2 — fine!
        s as f64 / c as f64
    }
    
    fn first_and_last(data: &[i32]) -> Option<(i32, i32)> {
        if data.is_empty() {
            None
        } else {
            Some((data[0], data[data.len() - 1]))
        }
    }
    
    // Multiple shared references can coexist
    fn demonstrate_multiple_borrows() {
        let data = vec![1, 2, 3, 4, 5];
        let r1 = &data;
        let r2 = &data;
        let r3 = &data;
        // All three references valid simultaneously
        println!("r1={:?}, r2={:?}, r3={:?}", r1[0], r2[1], r3[2]);
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_average() {
            assert!((average(&[1, 2, 3, 4, 5]) - 3.0).abs() < 0.001);
        }
    
        #[test]
        fn test_first_and_last() {
            assert_eq!(first_and_last(&[10, 20, 30]), Some((10, 30)));
            assert_eq!(first_and_last(&[]), None);
        }
    
        #[test]
        fn test_multiple_borrows() {
            let v = vec![1, 2, 3];
            let r1 = &v;
            let r2 = &v;
            assert_eq!(r1.len(), r2.len());
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_average() {
            assert!((average(&[1, 2, 3, 4, 5]) - 3.0).abs() < 0.001);
        }
    
        #[test]
        fn test_first_and_last() {
            assert_eq!(first_and_last(&[10, 20, 30]), Some((10, 30)));
            assert_eq!(first_and_last(&[]), None);
        }
    
        #[test]
        fn test_multiple_borrows() {
            let v = vec![1, 2, 3];
            let r1 = &v;
            let r2 = &v;
            assert_eq!(r1.len(), r2.len());
        }
    }

    Deep Comparison

    Core Insight

    Shared borrows allow multiple simultaneous readers — the compiler guarantees no one writes during shared access

    OCaml Approach

  • • See example.ml for implementation
  • Rust Approach

  • • See example.rs for implementation
  • Comparison Table

    FeatureOCamlRust
    Seeexample.mlexample.rs

    Exercises

  • Write a stats(data: &[f64]) -> (f64, f64, f64) function that returns (min, max, mean) using three separate passes over the borrowed slice.
  • Create a struct ReadOnlyView<'a, T> { data: &'a [T] } that exposes read-only operations and confirm you cannot store a &mut [T] in it.
  • Demonstrate that passing &data to two concurrent threads (using std::thread::scope) works without locks when neither thread mutates the data.
  • Open Source Repos