Hi, kind of a beginner question here. I'm trying to write a function that looks like this:
fn apply_jump_request(game: &mut GameState) -> {
if let Some(raid) = &mut game.raid {
let position = queries::compute_encounter(game)
raid.encounter = Some(position);
}
}
fn compute_encounter(game: &GameState) -> u32 { ... }
But the borrow checker won't let me do this, I can't invoke compute_position() while I have a mutable borrow open. Assuming I don't want to move this computation outside of the if branch, what is the best design option here? Should I just do a non-mutable borrow and then
Since game.raid is already an Option, you could use take() to move it out and then put it back in after the call to compute_encounter(). This assumes you don't need raid inside compute_encounter(); if you do, then you will need to pass it in separately.
split GameState into smaller structs, so that you can call functions on disjoint fields queries::compute_encounter(&game.only_compute_part).
wrap raid field in RefCell or Mutex, which will let you borrow it while using &GameState elsewhere.
defer mutation until next frame. Bigger game engines often avoid modifying game state during a frame, and have a queue of actions to perform between frames. queue.push(SetRaidPosition(position)).
this kind of problem generally indicates design choices that are not well suited for rust programming paradigms. most such problems come from the habit of putting too many states into a single struct and trying to implement all operations as methods on the that struct. within such methods, all the states are borrowed because of self, so you'll have to find a way to circumvent the borrow checker, either to prefer performance but risk safety (e.g. MaybeUninit, UnsafeCell), or pay a little runtime overhead to use the safe Option, RefCell, Cell, etc.
if you have type names containing words like Application, Engine, Manager etc, be extra careful not to fall into what I call the "borrow whole program" situation.
as one gains more experience in rust, it is not too hard to adapt to design patterns that are more fitting for rust, but for existing codebase, it's really hard to move away from legacy architectural limitations. if the solutions proposed above won't fit your problem, there's the partial-borrow crate. it's the sledgehammer if you think your problems are all nails.
it use procedural macro to generate projected proxy types which you can borrow individual fields with different modes. using OP's problem as example:
#[derive(PartialBorrow)]
struct GameState {
raid: Option<u32>,
field_a: i32,
field_b: i32,
//...
}
/// this function requires exclusive access to the whole GameState
fn apply_jump_request(game: &mut GameState) {
// split GameState into `part`1 and `part2`
// we specify `part1` mut borrows a single `raid` field
// part2 will have calculated type with the "left over" fields and mutability
let (part1, part2) =
SplitOff::<partial!(GameState mut raid, !*)>::split_off_mut(game);
if let Some(raid) = part1.raid.as_mut() {
// compute_encounter need const borrows excluding `raid`
// we have part2 with mut borrows, which satisfy the type requirement
let position = compute_encounter(part2.as_ref());
todo!()
}
}
/// this function don't need the full GameState,
/// we explicitly excluded the `raid` field
/// other fields need immutable borrow (note the `const` keyword)
fn compute_encounter(game: &partial!(GameState ! raid, const *)) -> u32 {
todo!()
}