I'm creating a program that lets you edit the metadata of of audio files (e.g., "artist", "title", etc.). A Track has a metadata HashMap<String, Ediit> where an Edit contains the original and current value of the metadata (this allows decoupling metadata edits and file saves).
I'm very new to Rust, and the issue I'm running into is finding a compiler-compliant and clean way for my app to maintain a mutable reference to an underlying Edit while the user is editing.
The original way I tried solving this problem was by using a chain of &mut self methods to return a &mut Edit from Track, but (1) this always results in multiple mutable references existing at once and (2) it seems odd using &mut self methods within Track for what should be immutable methods:
use std::collections::HashMap;
pub struct Edit {
original: Option<String>,
current: Option<String>,
}
impl Edit {
pub fn delete(&mut self) {}
pub fn update(&mut self) {}
pub fn value(&self) -> &str {
return match &self.current {
Some(v) => &v[..],
None => "",
};
}
}
pub struct Track {
metadata: HashMap<String, Edit>,
}
impl Track {
pub fn get_artist(&mut self) -> Option<&mut Edit> {
return self.get("Artist");
}
pub fn get_title(&mut self) -> Option<&mut Edit> {
return self.get("Title");
}
// A private get() method to keep DRY.
fn get(&mut self, k: &str) -> Option<&mut Edit> {
return self.metadata.get_mut(k);
}
pub fn remove(&mut self, k: &str) {
self.metadata.remove(k);
}
}
fn main() {
let mut track = Track {
metadata: HashMap::new(),
};
// Assume these `Option`s don't come back as None.
// In this example, it will compile if I update `Edit.value()` to return a `String`,
// But my actual app state needs to maintain mutable references to multiple `Edit`s in a `Track`.
let title: &str = track.get_title().unwrap().value(); // fist mutable borrow
let artist: &str = track.get_artist().unwrap().value(); // ERROR: cannot borrow `track` as mutable more than once at a time
println!("{} by {}", title, artist);
}
Some alternatives I've thought of:
Option 1: Using RefCell... somewhere.
I'm unsure if my use case meets one of three situations the Rust docs recommend using inherited mutability.
I can't have Track.get() return RefCell<Option<&mut Edit>> or Option<RefCell<&mut Edit>> because the presence of &mut Edit presents the same problem as before; this type also looks a little bit silly.
I could skip having to chain &mut self methods by changing the type of Edit.current to RefCell<Option<String>>, allowing me to have Edit.update() and Edit.delete() now use &self instead of &mut self. However, this feels disingenuous to the caller as these methods are clearly mutating the struct, unless these methods qualify as "logically immutable"?
I could change the type of Track.metadata toHashMap<String, RefCell<Edit>>. This feels the most congruent because it allows me to stop using &mut self within Track methods while also keeping &mut self within Edit. I'm not sure if this is a good use case of "introducing mutability inside of something immutable" or if I'm just using it to circumvent the language?
Option 2: Refactoring my approach.
Perhaps I'm not thinking in Rust well enough? Is there something I'm missing about the way I could be structuring my program? Is my approach to the Edit and Track data structures wrong? Is my philosophy on data flow wrong?
As a not-so-great (broken) example, I could change the contract so that:
Editimplements theClonetrait.Track.lookupnow returnsOption<Edit>(where the wrappedEditis a clone of the originalEdit).- Add a method like
Track.set(&mut self, k: String, v: Option<String>).
(This is broken because it is possible for the clone to still exist even if I were to call Track.remove(). References would prevent this!)