Andreas Morhammer
Andreas Morhammer

Reputation: 17

Rust lifetime confusion

I'm currently teaching myself Rust and am practicing by implementing Tic-Tac-Toe.

I have a Board struct (Cell and GameState are straightforward enums, SIZE is 3 usize):

struct Board {
    state : GameState,
    cells : [[Cell; SIZE]; SIZE],
}

and I'm trying to implement a mark method:

impl Board {
    fn mark(&mut self, pos: &Position, player: Player) {
        // various checks omitted
        // mark cell for player
        self.cells[pos.row][pos.col] = Cell::Filled(player);
        // check whether player has won either via a full row or column (diagonals omitted):
        if (0..SIZE).map(|i| &self.cells[pos.row][i]).all(|c| *c == Cell::Filled(player)) ||
           (0..SIZE).map(|i| &self.cells[i][pos.col]).all(|c| *c == Cell::Filled(player)) {
            self.state = GameState::Winner(player);
    }
}

so far so good... but there's the ugly code repetition.

so my next step was introducing a closure and replacing the repetition in the if with it:

let all_in_line = |cell_access| (0..SIZE).map(cell_access).all(|c : &Cell| *c == Cell::Filled(player));

if all_in_line(|i : usize| &self.cells[pos.row][i]) ||
   all_in_line(|i : usize| &self.cells[i][pos.col]) {

that does not work though because it would require for all_in_line to be generic (since the two closures that I pass to it have a different, anonymous type), so my next step was to do something akin to type erasure:

fn mark(&mut self, pos: &Position, player: Player) {
    let all_in_line = |cell_access : Box<dyn Fn(usize) -> &Cell>| (0..SIZE).map(cell_access).all(|c : &Cell| *c == Cell::Filled(player));
    if all_in_line(Box::new(|i : usize| &self.cells[pos.row][i])) ||
       all_in_line(Box::new(|i : usize| &self.cells[i][pos.col])) {
        self.state = GameState::Winner(player);
    }

But now the rust compiler complains about the lack of a lifetime parameter for the returned reference in the type specifier of cell_access:

error[E0106]: missing lifetime specifier
let all_in_line = |cell_access : Box<dyn Fn(usize) -> &Cell>| (0..SIZE).map(cell_access).all(|c : &Cell| *c == Cell::Filled(player));
                                                      ^ expected named lifetime parameter

I've tried modifying mark to fn mark<'a>(&'a mut self, /*...*/) and updating cell_access's type accordingly: Box<dyn Fn(usize) -> &'a Cell> but this fails:

error[E0597]: `self` does not live long enough
fn mark<'a>(&'a mut self, pos: &Position, player: Player) {
        -- lifetime `'a` defined here
...
    if all_in_line(Box::new(|i : usize| &self.cells[pos.row][i]))
                           ----------- -^^^^------------------
                           |           ||
                           |           |borrowed value does not live long enough
                           |           returning this value requires that `self` is borrowed for `'a`
                           value captured here
...
}
- `self` dropped here while still borrowed

At this point, I've run out of ideas what's wrong and how to fix it.

Upvotes: 0

Views: 331

Answers (1)

Kevin Reid
Kevin Reid

Reputation: 43842

But now the rust compiler complains about the lack of a lifetime parameter for the returned reference in the type specifier of cell_access:

The problem here, as I understand it, is that in order to be able to write the code in this shape, the cell_access function's signature needs to refer to the lifetime for which it is valid, and this is impossible because that lifetime doesn't have a name — it's a local reborrow of self that's implicit in the closure creation. Your attempt with &'a mut self doesn't work because it doesn't capture the fact that the closure's self is a reborrow of the function's self and accordingly does not need to live exactly the same lifetime, since mutable borrows' lifetimes are invariant rather than covariant; they can't be arbitrarily taken as shorter (because that would break the exclusiveness of mutable references).

That last point gives me an idea for how to fix this: move the all_in_line code into a function which takes self by immutable reference. This compiles:

impl Board {
    fn mark<'a>(&'a mut self, pos: &Position, player: Player) {
        if self.all_in_line_either_direction(pos, player)
        {
            // self.state = GameState::Winner(player);
        }
    }
    
    fn all_in_line_either_direction<'a>(&'a self, pos: &Position, player: Player) -> bool {
        let all_in_line = |cell_access: Box<dyn Fn(usize) -> &'a Cell>| {
            (0..SIZE)
                .map(cell_access)
                .all(|c: &Cell| *c == Cell::Filled(player))
        };
        all_in_line(Box::new(|i: usize| &self.cells[pos.row][i]))
            || all_in_line(Box::new(|i: usize| &self.cells[i][pos.col]))
    }
}

However, while the above does work, I think the best option here is to, instead of trying to make all_in_line a closure, make it a separate function (which can be generic), and avoid using a Box<dyn Fn> at all.

In this position, we can write out the lifetime we actually need — the function's lifetime generic <'a> implies that the lifetime 'a lasts as long as all_in_line needs it (a lifetime parameter can't end in the middle of the function call), whereas the closure taking a boxed function trait doesn't have its own generic scope.

fn all_in_line<'a, F>(player: Player, cell_access: F) -> bool
where
    F: Fn(usize) -> &'a Cell
{
    (0..SIZE).map(cell_access).all(|c| *c == Cell::Filled(player))
}

Then you have to pass player to each call, but there's none of the other complications.

If you like, you can even write the definition of all_in_line inside the mark function so it doesn't exist outside. Whether that's more readable code is up to your decision.


I added some pieces to be able to check that the code compiled; hopefully this all matches what you're actually doing:

#[derive(Copy, Clone, Eq, PartialEq)]
struct Player;

struct Position {
    row: usize,
    col: usize,
}

#[derive(Eq, PartialEq)]
enum Cell {
    Filled(Player),
}

const SIZE: usize = 10;

struct Board {
    // state: GameState,
    cells: [[Cell; SIZE]; SIZE],
}

impl Board {
    fn mark(&mut self, pos: &Position, player: Player) {
        self.cells[pos.row][pos.col] = Cell::Filled(player);
        if all_in_line(player, |i: usize| &self.cells[pos.row][i])
            || all_in_line(player, |i: usize| &self.cells[i][pos.col])
        {
            // self.state = GameState::Winner(player);
        }
    }
}

fn all_in_line<'a, F>(player: Player, cell_access: F) -> bool
where
    F: Fn(usize) -> &'a Cell,
{
    (0..SIZE)
        .map(cell_access)
        .all(|c| *c == Cell::Filled(player))
}

Upvotes: 2

Related Questions