I have a construct similar to the following where the Analysis
struct is responsible for analysing a file and the Project
struct is responsible for managing files and dependencies that serves as a minimal reproducible example:
struct File;
trait Project {
fn get_or_insert(&mut self) -> &File;
}
struct Analysis<'a> {
project: &'a mut dyn Project
}
impl Analysis<'_> {
fn analyze(&mut self) {
let file = self.project.get_or_insert();
self.analyze_parsed_file(file);
}
pub fn analyze_parsed_file(
&mut self,
file: &File
) {}
}
What I want to do is analyse a file. The project internally has an in-memory list of loaded files, but if the file I want to get is not inside that list, the project will load it from the disk and save it to that in-memory representation. The saving is necessary (in my case for cyclic dependency analysis).
The provided example does not compile because I am attempting to borrow self
as mutable more than once. My rationale behind this is the following (but maybe that's already where my understanding of the borrow checker is at its limits):
If I call analyze_parsed_file
, the function could modify file
through the mutable reference to project
. Therefore, the example is illegal in rust.
Now I know that this will never happen, because the project will never mutate files – it will only ever add them to the internal storage and it will not overwrite the file I just got.
Therefore, my question basically is: How do I tell this the borrow checker? Probably there are ways around this issue by using some form of unsafe
, but I would rather stay in safe territory given that this seems to be a fairly straightforward use case. Is this where the ominous RefCell
comes into play?
As I said in the comments, you need to make Project::get_or_insert()
takes &self
instead of &mut self
.
How do we do that, while it needs to mutate self
?
The key is this:
the project will never mutate files – it will only ever add them to the internal storage and it will not overwrite the file I just got
If, for example, you store your files in a Vec
- and assuming you have some form of indirection, e.g. Vec<Box<File>>
(otherwise growing the Vec
would invalidate all existing files) - you are good to go. But the compiler doesn't know that.
The solution is to use a special collection, a collection that pushes with shared references. Such collection is sound as long as it does not allow removal (it can allow it with mutable references, though). And it can be written using unsafe code.
Fortunately, you don't need to write that unsafe code - since someone else already did! Enter the elsa
crate, that provides append-only collections of various kinds.
So, assuming you have for example Vec<Box<File>>
, replace it with elsa::FrozenVec<Box<File>>
, replace &mut self
with &self
, and that is all!