ExamplesBy LevelBy TopicLearning Paths
897 Intermediate

897-borrowing-shared — Shared Borrowing (&T)

Functional Programming

Tutorial

The Problem

Reading data from multiple places simultaneously is safe as long as no one is writing. Rust formalizes this with shared references (&T): unlimited readers can hold &T simultaneously, but no writer can hold &mut T while any &T exists. This compile-time "readers-writer lock" prevents data races without runtime overhead. OCaml's GC and immutability-by-default avoid the need for explicit borrowing — values are implicitly shared. Rust's borrow checker is the mechanism that makes systems-level Rust safe without garbage collection.

🎯 Learning Outcomes

  • • Use &T to borrow data without transferring ownership
  • • Understand the "multiple readers, zero writers" rule enforced at compile time
  • • Pass &str and &[T] to functions to avoid unnecessary cloning
  • • Recognize that functions accepting &[T] are more flexible than those accepting Vec<T>
  • • Compare with OCaml's GC-based sharing where borrowing is not a programmer concern
  • Code Example

    // &str borrows a String without taking ownership
    pub fn string_info(s: &str) -> usize {
        s.len()
    }
    
    pub fn sum_slice(data: &[i32]) -> i32  { data.iter().sum() }
    pub fn max_slice(data: &[i32]) -> Option<i32> { data.iter().copied().reduce(i32::max) }
    pub fn min_slice(data: &[i32]) -> Option<i32> { data.iter().copied().reduce(i32::min) }
    
    // Three simultaneous shared borrows — all legal because none mutate
    pub fn stats(data: &[i32]) -> (i32, Option<i32>, Option<i32>) {
        (sum_slice(data), max_slice(data), min_slice(data))
    }

    Key Differences

  • Compile-time enforcement: Rust's borrow checker prevents aliased mutation at compile time; OCaml relies on immutable-by-default and runtime GC.
  • Explicit annotation: Rust requires &T in function signatures to indicate borrowing; OCaml passes pointers implicitly.
  • Lifetime tracking: Rust tracks how long borrows live to prevent use-after-free; OCaml's GC prevents this at runtime.
  • Slice vs list: Rust &[T] borrows a contiguous slice zero-copy; OCaml list is a linked structure — no zero-copy subslice without the Array type.
  • OCaml Approach

    OCaml has no equivalent borrowing concept. Passing a list or array to a function shares a pointer — the runtime ensures safety via GC. Multiple functions can read the same list simultaneously without annotation. OCaml's functional style and immutable defaults mean sharing is safe by default. For mutable data (ref, Array), OCaml relies on the programmer to avoid concurrent mutation — there is no compile-time check like Rust's borrow checker for sequential code.

    Full Source

    #![allow(clippy::all)]
    // Example 103: Shared References (&T)
    //
    // Shared borrows let multiple readers access data without transferring ownership.
    // Rule: unlimited &T borrows allowed simultaneously, but no &mut T while &T exists.
    // This enforces "multiple readers, zero writers" at compile time with zero runtime cost.
    
    // Approach 1: Borrowing instead of moving — &str borrows the String in place
    pub fn string_info(s: &str) -> usize {
        let len = s.len();
        let upper = s.to_uppercase();
        // Returns len so caller can use both the original string and the result
        let _ = upper; // used for demonstration; in real code you'd return or log it
        len
    }
    
    // Approach 2: Multiple shared readers of a slice — each function borrows independently
    pub fn sum_slice(data: &[i32]) -> i32 {
        data.iter().sum()
    }
    
    pub fn max_slice(data: &[i32]) -> Option<i32> {
        data.iter().copied().reduce(i32::max)
    }
    
    pub fn min_slice(data: &[i32]) -> Option<i32> {
        data.iter().copied().reduce(i32::min)
    }
    
    /// Compute sum, max, and min from the same slice using three simultaneous borrows.
    /// All three references coexist because none of them mutate `data`.
    pub fn stats(data: &[i32]) -> (i32, Option<i32>, Option<i32>) {
        let s = sum_slice(data); // borrow 1
        let mx = max_slice(data); // borrow 2
        let mn = min_slice(data); // borrow 3
        (s, mx, mn)
    }
    
    // Approach 3: Shared reference prevents accidental mutation
    // Accepting &[T] instead of Vec<T> signals "read only, no ownership transfer"
    pub fn contains_duplicate(data: &[i32]) -> bool {
        (1..data.len()).any(|i| data[..i].contains(&data[i]))
    }
    
    // Approach 4: Nested shared references — borrowing a reference to a reference
    pub fn first_char(s: &str) -> Option<char> {
        s.chars().next()
    }
    
    pub fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
        if a.len() >= b.len() {
            a
        } else {
            b
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_string_info_borrows_without_moving() {
            let msg = String::from("hello world");
            let len1 = string_info(&msg); // borrow, not move
            let len2 = string_info(&msg); // can borrow again — msg still alive
            assert_eq!(len1, len2);
            assert_eq!(len1, 11);
            // msg is still accessible here — the borrows ended
            assert_eq!(msg.len(), 11);
        }
    
        #[test]
        fn test_multiple_simultaneous_borrows() {
            let data = [3, 1, 4, 1, 5, 9, 2, 6];
            let (s, mx, mn) = stats(&data);
            assert_eq!(s, 31);
            assert_eq!(mx, Some(9));
            assert_eq!(mn, Some(1));
        }
    
        #[test]
        fn test_empty_slice() {
            let data: &[i32] = &[];
            assert_eq!(sum_slice(data), 0);
            assert_eq!(max_slice(data), None);
            assert_eq!(min_slice(data), None);
        }
    
        #[test]
        fn test_contains_duplicate() {
            assert!(contains_duplicate(&[1, 2, 3, 2]));
            assert!(!contains_duplicate(&[1, 2, 3, 4]));
            assert!(!contains_duplicate(&[]));
            assert!(!contains_duplicate(&[42]));
        }
    
        #[test]
        fn test_longest_shared_lifetime() {
            let s1 = String::from("longer string");
            let result;
            {
                let s2 = String::from("short");
                result = longest(s1.as_str(), s2.as_str());
                // Both borrows valid here — result lifetime tied to the shorter scope
                assert_eq!(result, "longer string");
            }
            // s2 dropped, but result was only used inside the block
            assert_eq!(s1, "longer string");
        }
    
        #[test]
        fn test_first_char() {
            assert_eq!(first_char("hello"), Some('h'));
            assert_eq!(first_char(""), None);
            assert_eq!(first_char("αβγ"), Some('α'));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_string_info_borrows_without_moving() {
            let msg = String::from("hello world");
            let len1 = string_info(&msg); // borrow, not move
            let len2 = string_info(&msg); // can borrow again — msg still alive
            assert_eq!(len1, len2);
            assert_eq!(len1, 11);
            // msg is still accessible here — the borrows ended
            assert_eq!(msg.len(), 11);
        }
    
        #[test]
        fn test_multiple_simultaneous_borrows() {
            let data = [3, 1, 4, 1, 5, 9, 2, 6];
            let (s, mx, mn) = stats(&data);
            assert_eq!(s, 31);
            assert_eq!(mx, Some(9));
            assert_eq!(mn, Some(1));
        }
    
        #[test]
        fn test_empty_slice() {
            let data: &[i32] = &[];
            assert_eq!(sum_slice(data), 0);
            assert_eq!(max_slice(data), None);
            assert_eq!(min_slice(data), None);
        }
    
        #[test]
        fn test_contains_duplicate() {
            assert!(contains_duplicate(&[1, 2, 3, 2]));
            assert!(!contains_duplicate(&[1, 2, 3, 4]));
            assert!(!contains_duplicate(&[]));
            assert!(!contains_duplicate(&[42]));
        }
    
        #[test]
        fn test_longest_shared_lifetime() {
            let s1 = String::from("longer string");
            let result;
            {
                let s2 = String::from("short");
                result = longest(s1.as_str(), s2.as_str());
                // Both borrows valid here — result lifetime tied to the shorter scope
                assert_eq!(result, "longer string");
            }
            // s2 dropped, but result was only used inside the block
            assert_eq!(s1, "longer string");
        }
    
        #[test]
        fn test_first_char() {
            assert_eq!(first_char("hello"), Some('h'));
            assert_eq!(first_char(""), None);
            assert_eq!(first_char("αβγ"), Some('α'));
        }
    }

    Deep Comparison

    OCaml vs Rust: Shared References (&T)

    Side-by-Side Code

    OCaml

    (* OCaml: immutable by default — all bindings are effectively "shared reads" *)
    let string_info s =
      let len = String.length s in
      let upper = String.uppercase_ascii s in
      Printf.printf "String '%s' has length %d, upper: %s\n" s len upper;
      len
    
    let () =
      let msg = "hello world" in
      let len1 = string_info msg in
      let len2 = string_info msg in   (* still available — OCaml never moves values *)
      assert (len1 = len2)
    
    (* Multiple readers of the same list *)
    let sum_list  lst = List.fold_left ( + ) 0 lst
    let max_list  lst = List.fold_left max min_int lst
    let min_list  lst = List.fold_left min max_int lst
    
    let stats lst = (sum_list lst, max_list lst, min_list lst)
    

    Rust (idiomatic — shared borrows, &T)

    // &str borrows a String without taking ownership
    pub fn string_info(s: &str) -> usize {
        s.len()
    }
    
    pub fn sum_slice(data: &[i32]) -> i32  { data.iter().sum() }
    pub fn max_slice(data: &[i32]) -> Option<i32> { data.iter().copied().reduce(i32::max) }
    pub fn min_slice(data: &[i32]) -> Option<i32> { data.iter().copied().reduce(i32::min) }
    
    // Three simultaneous shared borrows — all legal because none mutate
    pub fn stats(data: &[i32]) -> (i32, Option<i32>, Option<i32>) {
        (sum_slice(data), max_slice(data), min_slice(data))
    }
    

    Rust (lifetime-annotated — explicit shared lifetimes)

    // 'a ties both inputs and the output to the same lifetime
    pub fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
        if a.len() >= b.len() { a } else { b }
    }
    

    Type Signatures

    ConceptOCamlRust
    Read-only string paramstring -> int (value copy / GC-managed)fn f(s: &str) -> usize
    Read-only list/sliceint list -> intfn f(data: &[i32]) -> i32
    Shared lifetimeimplicit (GC)fn f<'a>(a: &'a str, b: &'a str) -> &'a str
    Optional result'a optionOption<T>

    Key Insights

  • Immutability is the default in OCaml; borrowing is the mechanism in Rust. OCaml values are immutable by default, so multiple readers coexist naturally under GC. Rust achieves the same safety without GC by tracking ownership and borrows at compile time.
  • **&T is a zero-cost compile-time read-lock.** While any &T exists, the compiler prevents any &mut T from existing. Iterator invalidation, data races, and use-after-free are ruled out structurally — not by runtime checks.
  • **Slices (&[T]) are the Rust equivalent of OCaml lists for read-only access.** OCaml's list is a persistent, GC-managed structure. Rust's &[T] is a fat pointer (data + length) that borrows any contiguous sequence without allocation.
  • Lifetime annotations make sharing contracts explicit. OCaml's GC hides lifetimes. Rust's 'a annotations express "this output lives at least as long as these inputs" — turning implicit GC guarantees into verifiable compiler contracts.
  • **Multiple simultaneous &T borrows are always safe.** sum_slice, max_slice, and min_slice can all hold a shared borrow of the same slice at once. The compiler knows none of them can mutate it, so no synchronization or copying is needed.
  • When to Use Each Style

    **Use &T (shared borrow) when:** you need read-only access to data that someone else owns — function parameters, iterator consumers, analysis functions, display/formatting logic.

    **Use &mut T (exclusive borrow) when:** you need to modify in place — sorting, filling a buffer, updating fields. The compiler ensures no &T borrows overlap with &mut T.

    **Use owned T when:** the function logically takes responsibility for the value — constructors, thread spawning, returning data to a new owner.

    Exercises

  • Write statistics(data: &[f64]) -> (f64, f64, f64, f64) returning (min, max, mean, variance) using only shared references.
  • Implement common_prefix<'a>(a: &'a str, b: &'a str) -> &'a str that returns the longest common prefix as a borrowed slice.
  • Write find_duplicates(data: &[i32]) -> Vec<i32> that returns only values appearing more than once, using only shared borrows.
  • Open Source Repos