3

I'm trying to write Tetris in rust. I have structs in this project that I want to treat as immutable even though they do mutate.

The approach I'm using to achieve this kind of behavior is this:

#[derive(Debug)]
struct Example {
    foo: i8
}

impl Example {
    fn change(mut self) -> Self {
        self.foo = 8;
        self
    }
}

which allows you to do stuff like this:

let first = Example { foo: 0 };
let second = first.change();

println!("{:?}", second); // Example { foo: 8 }

but yells at you when you do things like this:

let first = Example { foo: 0 };
let second = first.change();
    
println!("{:?}", first); // error[E0382]: borrow of moved value: `first`

The part where I'm confused is, why does this work:

#[derive(Debug)]
struct Matrix {
    cells: [[char; 2]; 2]
}

impl Matrix {
    fn new() -> Self {
        Matrix {
            cells: [['░'; 2]; 2]
        }    
    }
    
    fn solidify(mut self, row: usize, column: usize) -> Self {
        self.cells[row][column] = '█';
        self
    }
}

fn main() {
    let matrix = Matrix::new();
    let matrix = matrix.solidify(0, 0);
    
    println!("{:?}", matrix); // Matrix { cells: [['█', '░'], ['░', '░']] }
}

when this doesn't?

#[derive(Debug)]
struct Matrix {
    cells: [[char; 2]; 2]
}

impl Matrix {
    fn new() -> Self {
        Matrix {
            cells: [['░'; 2]; 2]
        }    
    }
    
    fn solidify(mut self, row: usize, column: usize) -> Self {
        self.cells[row][column] = '█';
        self
    }
}

#[derive(Debug)]
struct Tetris {
    matrix: Matrix
}

impl Tetris {
    fn new() -> Self {
        Tetris {
            matrix: Matrix::new()
        }
    }
    
    fn change(&mut self) {
        self.matrix = self.matrix.solidify(0, 0); 
/*      -----------------------------------------
        This is where it yells at me ^                 */
    } 
}

fn main() {
    let mut tetris = Tetris::new();
    tetris.change();
    
    println!("{:?}", tetris); // error[E0507]: cannot move out of `self.matrix` which is behind a mutable reference
}

Playground

This gives:

error[E0507]: cannot move out of `self.matrix` which is behind a mutable reference
  --> src/main.rs:32:23
   |
32 |         self.matrix = self.matrix.solidify(0, 0); 
   |                       ^^^^^^^^^^^ -------------- `self.matrix` moved due to this method call
   |                       |
   |                       move occurs because `self.matrix` has type `Matrix`, which does not implement the `Copy` trait
   |
note: `Matrix::solidify` takes ownership of the receiver `self`, which moves `self.matrix`
  --> src/main.rs:13:21
   |
13 |     fn solidify(mut self, row: usize, column: usize) -> Self {
   |                     ^^^^

I've done some research and I feel like either std::mem::swap, std::mem::take, or std::mem::replace,

could do the trick for me, but I'm not exactly sure how.

  • 1
    Most likely you just want to take self by reference `&mut self` instead of `mut self` and not return anything in both `solidify` and `Example::change`. [Chapter 4 on ownership](https://doc.rust-lang.org/stable/book/ch04-00-understanding-ownership.html) explains this if you haven't read it I recommend to do so now. – cafce25 Mar 13 '23 at 08:40
  • How do I return `self` from `solidify` when I take `self` by an `&mut` reference? The compiler yells at me saying `expected struct Matrix, found &mut Matrix` – eliaxelang007 Mar 13 '23 at 08:47
  • As put above "and not return anything". If you modify them in place you don't really have to return something for those methods. – cafce25 Mar 13 '23 at 08:49
  • Oops, sorry, didn't read your question right. I was just modifying them in place before, but I wanted to make my code more *functional* while still maintaining the performance benefits of mutating a variable in place instead of cloning all the time. – eliaxelang007 Mar 13 '23 at 09:02
  • another solution is to implement copy, https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=e641ee887397f718c6b7f2aa9d097825, also you may want to take matrix by mut ref too. – Stargateur Mar 13 '23 at 11:51

2 Answers2

3

You're right. mem::[take,replace]() can do the work.

The problem is that while you can leave a variable uninitialized for a time, you cannot leave a mutable reference uninitialized for a time (by moving out of it), even if you reassign it after.

There is a reason to this limitation: panics. If matrix.solidify() panics, we will exit without executing the recurring assignment to matrix. Later, we could recover from the panic, and observe the moved-from matrix.

Without dependencies (and unsafe code), the only solution is to leave something behind even when we reassign, so that even if we panic matrix stays initialized. std::mem::take() can help with that if Matrix implements Default - it leaves the default value, while the more general std::mem::replace() can help otherwise - it leaves some value:

#[derive(Debug, Default)]
struct Matrix {
    cells: [[char; 2]; 2]
}

impl Tetris {
    fn change(&mut self) {
        let matrix = std::mem::take(&mut self.matrix);
        self.matrix = matrix.solidify(0, 0); 
    } 
}

Or:

#[derive(Debug)] // No `Default`.
struct Matrix {
    cells: [[char; 2]; 2]
}

impl Tetris {
    fn change(&mut self) {
        let matrix = std::mem::replace(&mut self.matrix, Matrix::new());
        self.matrix = matrix.solidify(0, 0); 
    } 
}

If this is not good enough for you (for example, because you don't have a good default value to insert, or because of performance requirements) you can use the replace_with crate. It provides replace_with::replace_with_or_abort(), that will just abort the whole process in case of a panic, preventing the possibility of recovering:

impl Tetris {
    fn change(&mut self) {
        replace_with::replace_with_or_abort(&mut self.matrix, |matrix| matrix.solidify(0, 0));
    }
}

Note that instead of what you're doing now, you may actually want interior mutability.

Chayim Friedman
  • 47,971
  • 5
  • 48
  • 77
  • Thanks! I think the `replace_with` crate is exactly what I was looking for. The `take` and `replace` functions essentially clone the variable to make it work, and that's exactly what I was trying to avoid. – eliaxelang007 Mar 13 '23 at 09:04
  • that for this kind of thing I really don't like panic in rust, "recover" from panic shouldn't be possible. – Stargateur Mar 13 '23 at 11:47
1

I've recently discovered that I don't really need the replace_with crate if I apply the same own mutable self and then return mutated self pattern to my change function.

All I had to do this whole time was change this:

impl Tetris {
    fn change(&mut self) {
        self.matrix = self.matrix.solidify(0, 0); 
/*      -----------------------------------------
        This is where it yells at me ^                 */
    }
}

to this:

impl Tetris {
    fn change(mut self) -> Self {
        self.matrix = self.matrix.solidify(0, 0);
/*      -----------------------------------------
        It doesn't yell at me here anymore ^   :D      */
        self
    }
}

and then use the new code like this:

fn main() {
    let tetris = Tetris::new();
    let tetris = tetris.change();
    
    println!("{:?}", tetris); // Tetris { matrix: Matrix { cells: [['█', '░'], ['░', '░']] } }
}