Hi all - I'm learning Rust at the moment by implementing the PragProg book Mazes for Programmers as an exercise. However, I'm running into some trouble refactoring some of my rust code into the more OO style that the book is written in - in the book, we start with a general "grid" class which contains "cells". Over time, the author adds in some inherited classes for both of these that I'm having trouble getting to work correctly through the trait system.
My current code base (https://github.com/rbudnar/rust-mazes/) appears to work well enough (although it's likely not rust-idiomatic). I'm attempting to refactor my "cell" struct to be behind a trait so that I can implement different kinds of cells (right now it's a square cell with N/S/E/W, I'm attempting to add polar cells at the moment). The current cell implementation is here: https://github.com/rbudnar/rust-mazes/blob/master/crate/src/grid/cell.rs
My problem lies in being able to link cells together after refactoring to a trait. I've defined a trait that looks like this:
pub type ICellStrong = Rc<RefCell<ICell>>;
pub type CellLinkWeak = Weak<RefCell<Cell>>;
pub type CellLinkStrong = Rc<RefCell<Cell>>;
pub trait ICell {
fn neighbors(&self) -> Vec<ICellStrong>;
fn links(&self) -> Vec<Option<ICellStrong>>;
fn link(&mut self, other: ICellStrong, bidir: bool); // <-- this method is causing problems with alreadyBorrowed errors
fn as_any(&self) -> &dyn Any;
...
}
I've tried a number of variations of the link method, all which look something similar to this:
fn link(&mut self, other: ICellStrong, bidir: bool) {
let _self: CellLinkStrong = self.self_rc.upgrade().unwrap();
if let Some(nl) = other.borrow().as_any().downcast_ref::<Cell>() {
let new_link = Rc::clone(&other);
self.links2.push(Some(new_link));
}
if bidir {
let _other: ICellStrong = self.self_rc.upgrade().unwrap() as ICellStrong;
other.borrow_mut().link(Rc::clone(&_other), false);
}
}
// ... in the maze generation algorithm code:
MazeAlgorithm for RecursiveBacktracker {
fn on(&self, grid: &dyn Grid, rng_generator: &dyn RngWrapper) {
...
let rand_neighbor = rand_element(&unvisited, rng_generator);
current.borrow_mut().link(Rc::clone(rand_neighbor), true); // <--- I believe this borrow is causing issues
}
Though the code does compile, running the above code winds up panicking with an already mutably borrowed: BorrowError
. My attempts to resolve this have basically just pushed the error to different places.
What happens, in short, is this:
- the algorithm grabs one cell, looks through its neighbors, and eventually picks a neighbor to link to.
- I mutably borrow the current cell and call the link method
- the current cell pushes a link to the other cell and calls the link method on the other cell to provide a two way link.
- when the second cell is being linked, the code throws the
already borrowed
error, since the algorithm has already borrowed the current cell to initiate the link process.
I found a few insightful gems here and here, but I haven't been able to complete the second linking call successfully.
... Aaaand of course, as luck would have it - as I was typing this, I finally came up with something that doesn't panic. Silly me - in my algorithm, no one said I have to recursively link the cells (that's just what the book does). I've split the link calls out so that I'm not borrowing on top of a borrowed ref, like this:
// maze algorithm code
current.borrow_mut().link(Rc::clone(rand_neighbor), true);
rand_neighbor.borrow_mut().link(Rc::clone(¤t), true);
//
// cell code for linking
fn link(&mut self, other: ICellStrong) {
let _self: CellLinkStrong = self.self_rc.upgrade().unwrap();
if let Some(nl) = other.borrow().as_any().downcast_ref::<Cell>() {
let _other: CellLinkWeak = Rc::downgrade(&Rc::clone(&nl.self_rc.upgrade().unwrap()));
self.links.push(Some(_other));
}
}
Of course, I did all that typing so I don't want to waste it. So I guess my question now would be - is there a better way to achieve what I'm doing, overall? In terms of the abstraction of the cell into a trait and allowing for polymorphic access to the cells by the various grid algorithms, that is (each algorithm does need some small slice of access to each kind of cell for manipulation purposes). I feel like I'm trying to force the OO paradigm on this and I might be giving myself more headaches than needed.
Oh and since I'm here... is there a common naming convention for traits? Interfaces in other languages are generally prepended with an I, as in ICell
that I've done here.
Any other feedback for my learning purposes is highly appreciated (either on the above, or from the repo)!