ExamplesBy LevelBy TopicLearning Paths
545 Intermediate

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