Reputation: 17
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
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