ExamplesBy LevelBy TopicLearning Paths
545 Intermediate GitHub

Split Borrows from Structs

Functional Programming

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

  • • How Rust's borrow checker tracks borrows at the field level for plain structs
  • • How get_refs(&mut self) -> (&mut f32, &mut f32, &[(f32, f32)]) enables simultaneous field borrows
  • • Why split borrows work for structs but not through a method call on self in general
  • • How split_at_mut and split_first_mut provide split borrows on slices
  • • Where split borrows matter: ECS systems, game state, embedded systems with shared hardware registers
  • Code 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

  • Field-level tracking: Rust's borrow checker tracks individual struct fields — an improvement from early Rust which tracked whole structs; OCaml has no borrow tracking at all.
  • Method boundary: When split-borrow logic is inside a method returning a tuple, Rust accepts it; when callers try to hold borrows and call other methods simultaneously, the checker may reject it.
  • Slice splits: Rust requires split_at_mut for safe mutable slice splits; OCaml array/Bigarray slices can be subindexed mutably without restriction.
  • ECS systems: Rust ECS frameworks (Bevy, Legion) use unsafe code or archetype-based storage to achieve the split-borrow patterns that game logic requires; OCaml ECS frameworks rely on runtime discipline.
  • 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);
        }
    }
    ✓ Tests Rust test suite
    #[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

  • OCaml: Mutable fields independent, no tracking
  • Rust: Compiler tracks field-level borrows
  • Rust: Can borrow different fields simultaneously
  • Rust: Prevents aliasing of same field
  • Both: Enable efficient in-place updates
  • Exercises

  • Physics update: Add a method 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.
  • Slice head/tail: Write a function 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 with two Vecs: Create a 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.
  • Open Source Repos