898-borrowing-mutable — Mutable Borrowing (&mut T)
Tutorial
The Problem
In-place mutation is often more efficient than allocating new values. Sorting a large array in place, incrementing a counter, reversing a buffer — all benefit from direct mutation. Rust allows mutable access via &mut T with one strict rule: only one &mut T can exist at a time, and no &T can coexist with it. This prevents data races and iterator invalidation — the bugs that plagued C++ with concurrent mutation. The rule "exclusive access implies safe mutation" is Rust's solution to the aliasing problem that makes optimizing languages hard.
🎯 Learning Outcomes
&mut T to mutate data through a reference without taking ownershipiter_mut() provides mutable iteration over a collectionref cells and Array for explicit mutable stateCode Example
pub fn increment(c: &mut Counter) {
c.count += 1;
}
pub fn sum_into(data: &[i32], total: &mut i32) {
for &x in data {
*total += x;
}
}
pub fn reverse_in_place(arr: &mut [i32]) {
let n = arr.len();
for i in 0..n / 2 {
arr.swap(i, n - 1 - i);
}
}Key Differences
&mut T at a time; OCaml allows multiple references to the same ref cell.&mut T in a function signature explicitly declares "this function may modify the argument"; OCaml has no such declaration.&mut [T] enables in-place algorithms on slices with zero overhead; OCaml arrays support the same via arr.(i) <- value.OCaml Approach
OCaml uses ref cells for mutable variables: let total = ref 0 in List.iter (fun x -> total := !total + x) xs. Arrays are mutable: Array.iteri (fun i x -> arr.(i) <- x * 2) arr. OCaml's Buffer.t is a mutable string builder. Unlike Rust, OCaml allows multiple references to the same mutable value — no "one writer" restriction. This means OCaml programs can have aliasing mutations that would be compile errors in Rust, potentially leading to harder-to-debug state bugs.
Full Source
#![allow(clippy::all)]
// Example 104: Mutable References (&mut T)
//
// Only ONE &mut T at a time. No &T while &mut T exists.
// This prevents data races at compile time.
// Approach 1: Mutable reference to a struct
pub struct Counter {
pub count: i32,
}
pub fn increment(c: &mut Counter) {
c.count += 1;
}
pub fn get_count(c: &Counter) -> i32 {
c.count
}
// Approach 2: Mutable reference for accumulation
// Takes a mutable reference to the total — the caller owns the value,
// and we write into it without taking ownership.
pub fn sum_into(data: &[i32], total: &mut i32) {
for &x in data {
*total += x;
}
}
// Approach 3: In-place mutation via &mut [T]
// Reverses a slice in place — no allocation, mutates through the reference.
pub fn reverse_in_place(arr: &mut [i32]) {
let n = arr.len();
for i in 0..n / 2 {
arr.swap(i, n - 1 - i);
}
}
// Approach 4: Exclusive access — demonstrates borrow checker rules.
// We can pass &mut to a helper and regain access after it returns.
pub fn double_all(values: &mut [i32]) {
for v in values.iter_mut() {
*v *= 2;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_counter_increment() {
let mut c = Counter { count: 0 };
increment(&mut c);
increment(&mut c);
increment(&mut c);
assert_eq!(get_count(&c), 3);
}
#[test]
fn test_counter_starts_at_zero() {
let c = Counter { count: 0 };
assert_eq!(get_count(&c), 0);
}
#[test]
fn test_sum_into_accumulates() {
let mut total = 0;
sum_into(&[1, 2, 3, 4, 5], &mut total);
assert_eq!(total, 15);
}
#[test]
fn test_sum_into_empty_slice() {
let mut total = 42;
sum_into(&[], &mut total);
assert_eq!(total, 42);
}
#[test]
fn test_sum_into_accumulates_across_calls() {
let mut total = 0;
sum_into(&[1, 2, 3], &mut total);
sum_into(&[4, 5], &mut total);
assert_eq!(total, 15);
}
#[test]
fn test_reverse_in_place_even() {
let mut arr = [1, 2, 3, 4];
reverse_in_place(&mut arr);
assert_eq!(arr, [4, 3, 2, 1]);
}
#[test]
fn test_reverse_in_place_odd() {
let mut arr = [1, 2, 3];
reverse_in_place(&mut arr);
assert_eq!(arr, [3, 2, 1]);
}
#[test]
fn test_reverse_in_place_single() {
let mut arr = [42];
reverse_in_place(&mut arr);
assert_eq!(arr, [42]);
}
#[test]
fn test_reverse_in_place_empty() {
let mut arr: [i32; 0] = [];
reverse_in_place(&mut arr);
assert_eq!(arr, []);
}
#[test]
fn test_double_all() {
let mut values = vec![1, 2, 3, 4];
double_all(&mut values);
assert_eq!(values, vec![2, 4, 6, 8]);
}
#[test]
fn test_double_all_empty() {
let mut values: Vec<i32> = vec![];
double_all(&mut values);
assert_eq!(values, vec![]);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_counter_increment() {
let mut c = Counter { count: 0 };
increment(&mut c);
increment(&mut c);
increment(&mut c);
assert_eq!(get_count(&c), 3);
}
#[test]
fn test_counter_starts_at_zero() {
let c = Counter { count: 0 };
assert_eq!(get_count(&c), 0);
}
#[test]
fn test_sum_into_accumulates() {
let mut total = 0;
sum_into(&[1, 2, 3, 4, 5], &mut total);
assert_eq!(total, 15);
}
#[test]
fn test_sum_into_empty_slice() {
let mut total = 42;
sum_into(&[], &mut total);
assert_eq!(total, 42);
}
#[test]
fn test_sum_into_accumulates_across_calls() {
let mut total = 0;
sum_into(&[1, 2, 3], &mut total);
sum_into(&[4, 5], &mut total);
assert_eq!(total, 15);
}
#[test]
fn test_reverse_in_place_even() {
let mut arr = [1, 2, 3, 4];
reverse_in_place(&mut arr);
assert_eq!(arr, [4, 3, 2, 1]);
}
#[test]
fn test_reverse_in_place_odd() {
let mut arr = [1, 2, 3];
reverse_in_place(&mut arr);
assert_eq!(arr, [3, 2, 1]);
}
#[test]
fn test_reverse_in_place_single() {
let mut arr = [42];
reverse_in_place(&mut arr);
assert_eq!(arr, [42]);
}
#[test]
fn test_reverse_in_place_empty() {
let mut arr: [i32; 0] = [];
reverse_in_place(&mut arr);
assert_eq!(arr, []);
}
#[test]
fn test_double_all() {
let mut values = vec![1, 2, 3, 4];
double_all(&mut values);
assert_eq!(values, vec![2, 4, 6, 8]);
}
#[test]
fn test_double_all_empty() {
let mut values: Vec<i32> = vec![];
double_all(&mut values);
assert_eq!(values, vec![]);
}
}
Deep Comparison
OCaml vs Rust: Mutable References (&mut T)
Side-by-Side Code
OCaml
(* Mutable record fields — mutation is opt-in per field *)
type counter = { mutable count : int }
let increment c = c.count <- c.count + 1
(* Ref cells — first-class mutable references *)
let sum_into total lst =
List.iter (fun x -> total := !total + x) lst
(* Mutable arrays — in-place mutation *)
let reverse_in_place arr =
let n = Array.length arr in
for i = 0 to n / 2 - 1 do
let tmp = arr.(i) in
arr.(i) <- arr.(n - 1 - i);
arr.(n - 1 - i) <- tmp
done
Rust (idiomatic)
pub fn increment(c: &mut Counter) {
c.count += 1;
}
pub fn sum_into(data: &[i32], total: &mut i32) {
for &x in data {
*total += x;
}
}
pub fn reverse_in_place(arr: &mut [i32]) {
let n = arr.len();
for i in 0..n / 2 {
arr.swap(i, n - 1 - i);
}
}
Rust (iterator style)
pub fn double_all(values: &mut Vec<i32>) {
for v in values.iter_mut() {
*v *= 2;
}
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Mutable struct field | type t = { mutable x : int } | struct T { x: i32 } + &mut T |
| Ref cell | int ref (ref 0, !r, :=) | &mut i32 (explicit, checked) |
| Mutable slice | int array (always mutable) | &mut [i32] (exclusive borrow) |
| Mutation operator | <- (fields), := (refs) | *r = ... or field = ... |
Key Insights
&mut T is unique — no two mutable aliases can coexist. OCaml's ref cells and mutable fields allow aliasing freely; the programmer bears responsibility for avoiding races.mutable. In Rust you declare a binding let mut x and pass &mut x — the mutability travels with the reference, not the type declaration.&mut replaces OCaml ref without allocation.** An OCaml ref is a heap-allocated box. Rust &mut i32 is a stack reference — zero overhead. The borrow checker makes this safe where OCaml needs garbage collection to manage ref-cell lifetimes.iter_mut() is the Rust analogue of List.iter with mutation.** OCaml's List.iter (fun x -> total := !total + x) reads x but writes through the captured ref. Rust's iter_mut() hands out &mut to each element directly, keeping all mutation explicit and checked.&mut T exists, the borrow checker forbids any shared &T references. This rule is the compile-time equivalent of a mutex — enforced without runtime cost and impossible to forget.When to Use Each Style
**Use &mut T (Rust) when:** you need to mutate a value in place without transferring ownership — counters, accumulators, in-place sorting, filling buffers. The exclusivity guarantee means you never need a lock for single-threaded code.
**Use ref / mutable fields (OCaml) when:** you need shared mutable state across closures or callbacks within a single-threaded context. OCaml's GC manages lifetimes so aliasing is safe (single-threaded), but you lose the static race-freedom guarantee Rust provides.
Exercises
normalize_in_place(data: &mut [f64]) that divides each element by the maximum value, mutating in place.fill_with<T: Clone>(data: &mut [T], value: T) that overwrites all elements with the given value.Queue<T> backed by Vec<T> with enqueue(&mut self, item: T) and dequeue(&mut self) -> Option<T> methods.