I've painted myself into a borrowchecking hole. How do I learn the right way to use Rust?

I think the architecture of my project might be past saving. I want to ask “how do I solve the error on line 287?” but I know full well that I’m probably sitting on top of a jenga tower of mutable refs and over-borrowed structs.

To share a recent hack for dodging the borrow checker, I have an object you call methods on to mutate its internal state. It wants to update a couple of different cells in a 2d grid, but before that it has to check some other cells in the grid. If everything looks good, then it does some record keeping on the board and then returns an event to the caller, signaling where the ship is going to move.

    pub fn naive_navigate(&mut self, ship: Ship, destination: &Position) -> Direction {
        let ship_position = &ship.position;

        let current_cell = &self.occupied[ship.position.y as usize][ship.position.x as usize];
        let is_yard = match current_cell.structure {
            Some(_) => true,
            None => false,
        };
        if ship.halite > 0 && is_yard {
            for direction in self.get_unsafe_moves(&ship_position, destination) {
                let target_pos = ship_position.directional_offset(direction);

                if self.is_safe(&target_pos) {
                    self.mark_unsafe(&target_pos, ship.clone());
                    self.mark_unsafe_friendly(&target_pos, ship.clone());
                    return direction;
                }
            }
        }

        Direction::Still
    }

This yields an error, however:

error[E0502]: cannot borrow `*self` as mutable because `self.occupied` is also borrowed as immutable
   --> src/hlt/navi.rs:297:21
    |
287 |         let current_cell = &self.occupied[ship.position.y as usize][ship.position.x as usize];
    |                             ------------- immutable borrow occurs here
...
297 |                     self.mark_unsafe(&target_pos, ship.clone());
    |                     ^^^^ mutable borrow occurs here
...
305 |     }
    |     - immutable borrow ends here

error[E0502]: cannot borrow `*self` as mutable because `self.occupied` is also borrowed as immutable
   --> src/hlt/navi.rs:298:21
    |
287 |         let current_cell = &self.occupied[ship.position.y as usize][ship.position.x as usize];
    |                             ------------- immutable borrow occurs here
...
298 |                     self.mark_unsafe_friendly(&target_pos, ship.clone());
    |                     ^^^^ mutable borrow occurs here
...
305 |     }
    |     - immutable borrow ends here

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0502`.

On a wild hunch, I inline some stuff into a single expression, so my state-check:

        let current_cell = &self.occupied[ship.position.y as usize][ship.position.x as usize];
        let is_yard = match current_cell.structure {
            Some(_) => true,
            None => false,
        };

Becomes a single let :

        let is_yard =
            match &self.occupied[ship.position.y as usize][ship.position.x as usize].structure {
                Some(_) => true,
                None => false,
            };

And now the compiler error is gone and my code runs.

Some problems that I need help fixing:

  1. This is an unreadable hack.
  2. What can I do to actually learn the borrow checker instead of work on messy projects and learn to layer terrible fixes to run away from it?
  3. How can I learn more Rust-appropriate architectures so that it feels like I’m working with Rust instead of wholly against it?

It seems like there’s a ray of hope in learning more about Entity Component Systems, but so far the premise of them seems like “what if instead of domain objects, you modeled everything as vectors of integers that hate each other?” Maybe the corresponding post is supposed to be more practical, but I’ve got some kind of a paradigm blindspot when you switch to an environment that’s totally different but you keep trying to solve problems in the way you’re familiar with.

Would love some guidance or advice for how I’m supposed to reframe this so Rust is fun again.

Thanks!

Your code will work as-is once NLL (non lexical lifetimes) hits stable, which will be soon.

In the meantime, you can write the code as:

let is_yard = {
   let current_cell = &self.occupied[ship.position.y as usize][ship.position.x as usize];
  current_cell.is_some()
};

You need current_cell just for that small scope, so put it into one. You can also one line the whole thing:

let is_yard = self.occupied[ship.position.y as usize][ship.position.x as usize].is_some();
2 Likes

I have a slightly better idea for why my original code was wrong after skimming this post a bit.

The ascii example is super helpful:

fn bar() {
    let mut data = vec!['a', 'b', 'c'];
    let slice = &mut data[..]; // <-+ lifetime today
    capitalize(slice);         //   |
    data.push('d'); // ERROR!  //   |
    data.push('e'); // ERROR!  //   |
    data.push('f'); // ERROR!  //   |
} // <------------------------------+

So when you assign data to slice, that’s a new reference to the data. New reference means a borrow, and lending the data through that reference means the reference is the only thing that the world can use to touch that data. Right?

Yeah, that’s right. The problem with lexical scopes is, as the name suggests, a reference stays alive until end of its scope, even if it’s otherwise dead (i.e. no code accesses the data through it) sooner; this is what NLL aims to fix by tracking liveness more accurately. Until then, however, you need to add the smaller lexical scope by hand.

So in other words, you could switch to the Rust beta channel and try again. Rust 2018 RC1 is available - announcements - Rust Internals