897-borrowing-shared — Shared Borrowing (&T)
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
&T to borrow data without transferring ownership&str and &[T] to functions to avoid unnecessary cloning&[T] are more flexible than those accepting Vec<T>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
&T in function signatures to indicate borrowing; OCaml passes pointers implicitly.&[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('α'));
}
}#[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
| Concept | OCaml | Rust |
|---|---|---|
| Read-only string param | string -> int (value copy / GC-managed) | fn f(s: &str) -> usize |
| Read-only list/slice | int list -> int | fn f(data: &[i32]) -> i32 |
| Shared lifetime | implicit (GC) | fn f<'a>(a: &'a str, b: &'a str) -> &'a str |
| Optional result | 'a option | Option<T> |
Key Insights
&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.&[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.'a annotations express "this output lives at least as long as these inputs" — turning implicit GC guarantees into verifiable compiler contracts.&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
statistics(data: &[f64]) -> (f64, f64, f64, f64) returning (min, max, mean, variance) using only shared references.common_prefix<'a>(a: &'a str, b: &'a str) -> &'a str that returns the longest common prefix as a borrowed slice.find_duplicates(data: &[i32]) -> Vec<i32> that returns only values appearing more than once, using only shared borrows.