Split Borrows from Structs
Tutorial Video
Text description (accessibility)
This video demonstrates the "Split Borrows from Structs" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. A common pattern in game engines and simulations: update the player's position while reading the enemy list from the same `GameState`. Key difference from OCaml: 1. **Field
Tutorial
The Problem
A common pattern in game engines and simulations: update the player's position while reading the enemy list from the same GameState. Naively, borrowing &mut self prevents reading any field — the entire struct is "mutably borrowed." But Rust's borrow checker actually tracks borrows at the field level, not just the struct level. Split borrows let you hold &mut to one field and & to another simultaneously, as long as they do not alias. This is critical for performance-sensitive code that avoids unnecessary cloning.
🎯 Learning Outcomes
get_refs(&mut self) -> (&mut f32, &mut f32, &[(f32, f32)]) enables simultaneous field borrowsself in generalsplit_at_mut and split_first_mut provide split borrows on slicesCode Example
pub struct GameState {
pub player_x: f32,
pub player_y: f32,
pub enemies: Vec<(f32, f32)>,
}
impl GameState {
// Split borrow: different fields simultaneously
pub fn get_refs(&mut self) -> (&mut f32, &mut f32, &[(f32, f32)]) {
(&mut self.player_x, &mut self.player_y, &self.enemies)
}
}Key Differences
split_at_mut for safe mutable slice splits; OCaml array/Bigarray slices can be subindexed mutably without restriction.OCaml Approach
OCaml records allow simultaneous access to multiple fields through references with no restriction:
type game_state = { mutable player_x: float; mutable player_y: float; mutable enemies: (float * float) list }
let update state dx dy =
state.player_x <- state.player_x +. dx;
state.player_y <- state.player_y +. dy
No borrowing concept exists — all fields are always accessible through the record reference.
Full Source
#![allow(clippy::all)]
//! Split Borrows from Structs
//!
//! Borrowing different fields simultaneously.
pub struct GameState {
pub player_x: f32,
pub player_y: f32,
pub enemies: Vec<(f32, f32)>,
pub score: u32,
}
impl GameState {
pub fn new() -> Self {
GameState {
player_x: 0.0,
player_y: 0.0,
enemies: Vec::new(),
score: 0,
}
}
/// Split borrow: &mut player position, &enemies
pub fn get_refs(&mut self) -> (&mut f32, &mut f32, &[(f32, f32)]) {
(&mut self.player_x, &mut self.player_y, &self.enemies)
}
/// Borrow different fields independently
pub fn update_position(&mut self, dx: f32, dy: f32) {
self.player_x += dx;
self.player_y += dy;
}
pub fn add_enemy(&mut self, x: f32, y: f32) {
self.enemies.push((x, y));
}
}
impl Default for GameState {
fn default() -> Self {
Self::new()
}
}
/// Demonstrate split borrowing.
pub fn split_borrow_demo(state: &mut GameState) {
let (px, py, enemies) = state.get_refs();
*px = 10.0;
*py = 20.0;
for (ex, ey) in enemies {
println!("Enemy at ({}, {})", ex, ey);
}
}
/// Two-field struct for simpler example.
pub struct Pair {
pub left: String,
pub right: String,
}
impl Pair {
pub fn get_both(&mut self) -> (&mut String, &mut String) {
(&mut self.left, &mut self.right)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_split_borrow() {
let mut state = GameState::new();
state.enemies.push((1.0, 2.0));
let (px, py, enemies) = state.get_refs();
*px = 5.0;
*py = 10.0;
assert_eq!(enemies.len(), 1);
}
#[test]
fn test_pair_split() {
let mut pair = Pair {
left: String::from("L"),
right: String::from("R"),
};
let (l, r) = pair.get_both();
l.push_str("eft");
r.push_str("ight");
assert_eq!(pair.left, "Left");
assert_eq!(pair.right, "Right");
}
#[test]
fn test_sequential_borrows() {
let mut state = GameState::new();
state.update_position(1.0, 1.0);
state.add_enemy(5.0, 5.0);
assert_eq!(state.player_x, 1.0);
assert_eq!(state.enemies.len(), 1);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_split_borrow() {
let mut state = GameState::new();
state.enemies.push((1.0, 2.0));
let (px, py, enemies) = state.get_refs();
*px = 5.0;
*py = 10.0;
assert_eq!(enemies.len(), 1);
}
#[test]
fn test_pair_split() {
let mut pair = Pair {
left: String::from("L"),
right: String::from("R"),
};
let (l, r) = pair.get_both();
l.push_str("eft");
r.push_str("ight");
assert_eq!(pair.left, "Left");
assert_eq!(pair.right, "Right");
}
#[test]
fn test_sequential_borrows() {
let mut state = GameState::new();
state.update_position(1.0, 1.0);
state.add_enemy(5.0, 5.0);
assert_eq!(state.player_x, 1.0);
assert_eq!(state.enemies.len(), 1);
}
}
Deep Comparison
OCaml vs Rust: Split Borrows
OCaml
(* No concept of split borrows — records are immutable or use refs *)
type game_state = {
mutable player_x: float;
mutable player_y: float;
enemies: (float * float) list ref;
}
(* Can mutate any field anytime *)
Rust
pub struct GameState {
pub player_x: f32,
pub player_y: f32,
pub enemies: Vec<(f32, f32)>,
}
impl GameState {
// Split borrow: different fields simultaneously
pub fn get_refs(&mut self) -> (&mut f32, &mut f32, &[(f32, f32)]) {
(&mut self.player_x, &mut self.player_y, &self.enemies)
}
}
Key Differences
Exercises
fn physics_step(&mut self) -> (&mut f32, &mut f32, &[(f32, f32)]) { ... } and write a loop that moves the player toward the nearest enemy using the split references.fn map_head_tail<T: Clone + std::fmt::Debug>(s: &mut [T], f: impl Fn(&mut T)) using split_first_mut that applies f to the head while printing the tail.struct Buffers { input: Vec<u8>, output: Vec<u8> } and a method returning (&mut Vec<u8>, &mut Vec<u8>) — then use both to implement a transform-in-place operation.