I'm using a globally accessible HashMap to store homogoneous entities, Roster, and Person.
Now my Roster structs also have a HashMap of entities, Roster.names, which is a subset of the entities inside the global HashMap, MY_ITEMS, so Roster.names⊂MY_ITEMS,
Naturally I want to get mutable references to MY_ITEMS so that they can "interact with" and mutate each other. The problem is that in the course of mutating some specific entities via a function, other entities might need to be updated as well as a side effect.
But within the context of match MY_ITEMS.lock().unwrap().get_pair_mut("roster 1", "roster 2"){...} we can no longer borrow or mutate anything from MY_ITEMS. We only have mutable access to the two entities we requested via get_pair_mut(). So any attempts to update some third value in MY_ITEMS within this scope will result in a WouldBlock.
Here's a generalization of the problem:
SET has A,B, and C
B.C = SET.C.clone()
get &mut A and &mut B from SET{
A.change(B){
updates(B.C)
updates(B)
}
}
SET.B.C !== SET.C
Here's some trivial code showing this design problem:
multi_mut = "0.1.3"
lazy_static = "1.4.0"
use lazy_static::lazy_static;
use std::sync::Mutex;
use std::collections::HashMap;
use multi_mut::{HashMapMultiMut};
//globally accessible Hashmap
lazy_static! {
static ref MY_ITEMS: Mutex<HashMap<String, Entity>> = Mutex::new(HashMap::new());
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Person{
pub roster_id: Option<String>,
name: String,
id: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Roster{
pub id: String,
pub names: HashMap<String, Entity>
}
//wrapper enum
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Entity{
Roster(Roster),
Person(Person),
}
impl Roster{
pub fn new(id: String, start_names: Vec<(String, Person)>)->Self{
let mut map = HashMap::new();
map.extend(start_names.clone().into_iter().map(|n|{
let mut altered = n.1.clone();
altered.roster_id = Some(id.clone());
//on adding a Person to a Roster, we also add them to the global map
MY_ITEMS.lock().unwrap().insert(n.0.clone(), Entity::Person(altered.clone()));
return (n.0, Entity::Person(altered))
}));
Roster{
id: id,
names: map
}
}
pub fn take_person(&mut self, from_roster: &mut Roster, to_take_id: String){
//take a person from the other roster
let person_entry = from_roster.names.remove_entry(&to_take_id).unwrap();
//alter their information to reflect a change in which roster the person belongs too
if let Entity::Person(mut p) = person_entry.1.clone(){
p.roster_id = Some(self.id.clone());
//update the roster with the changed persons value
self.names.insert(person_entry.0.clone(), Entity::Person(p.clone()));
//trying to insert the updated value into the global hashmap will cause a lock to fail
//so value of person inside of this roster and inside of the global map becomes out of sync
// MY_ITEMS.try_lock().unwrap().insert(person_entry.0.clone(), Entity::Person(p.clone()));
}
}
}
fn main(){
//starting people for roster 1
let group_1 = vec![
(String::from("Person 1"), Person{roster_id: None, name: String::from("Josie"), id: String::from("Person 1")}),
(String::from("Person 2"), Person{roster_id: None, name: String::from("Joon"), id: String::from("Person 2")}),
(String::from("Person 3"), Person{roster_id: None, name: String::from("Glooby"), id: String::from("Person 3")})];
let roster_1 = Roster::new(String::from("roster 1"), group_1);
//starting people for roster 2
let group_2 = vec![
(String::from("Person 4"), Person{roster_id: None, name: String::from("Borg"), id: String::from("Person 4")}),
(String::from("Person 5"), Person{roster_id: None, name: String::from("Snog"), id: String::from("Person 5")}),
(String::from("Person 6"), Person{roster_id: None, name: String::from("Cloob"), id: String::from("Person 6")})];
let roster_2 = Roster::new(String::from("roster 2"), group_2);
//add rosters to the global map
MY_ITEMS.lock().unwrap().insert(roster_1.id.clone(), Entity::Roster(roster_1.clone()));
MY_ITEMS.lock().unwrap().insert(roster_2.id.clone(), Entity::Roster(roster_2.clone()));
//remove Person 4, Borg, from roster 2 and insert them into roster 1
match MY_ITEMS.lock().unwrap().get_pair_mut("roster 1", "roster 2"){
//becuase of get_pair_mut, we can no longer lock MY_ITEMS in this block scope
Some((r1_wrapper, r2_wrapper))=>{
match (r1_wrapper, r2_wrapper){
(Entity::Roster(r1), Entity::Roster(r2))=>{
r1.take_person(r2, String::from("Person 4"));
}
_=>()
}
}
_=>()
};
if let Entity::Roster(r1) = MY_ITEMS.lock().unwrap().get("roster 1").unwrap(){
//the roster_id field for Person 4 is updated in our roster_2 local variable
dbg!(r1.names.get("Person 4"));//->Some(Person)
//but it is not updated in our global map
dbg!(roster_1.clone().names.get("Person 4"));//->None
assert_eq!(r1.names.get("Person 4"), roster_1.names.get("Person 4"), "Roster value in local does not equal roster value in global");
}
}
So what could we do to keep the values we're using in our structs in sync with the values in our global map?
A couple of thoughts:
- It seems that we'd either have to know before mutating any
values in
MY_ITEMS, that we'd need to know all the items we'd like mutate. - multi_mut does have a function for iterating over an arbitrary
number of HashMap values,
iter_multi_mut(keys, buffer), but this doesn't seem like avery flexible design. - We could also potentially have
match MY_ITEMS.lock().unwrap().get_pair_mut()return some kind ofFnMut(Mutex<HashMap<String, Entitity>>)that would perform the side effects for us. - We could also send an event over a channel with the update information.
As a side note I think its important to keep all of People structs and all of our Roster structs in the same globally accessible variable. But maybe there is some other container struct besides a HashMap that would better accommodate the dependencies between items.
Another sidenote, because rust's HashMap has no convenient way of mutating multiple values in a HashMap, I'm using the multi_mut crate's get_pair_mut() method to get my mutable references to two entities in MY_ITEMS.
Maybe there is a different pattern or design which would be better suited to this?
- Edit: Here's a playground example of using Arc<Mutex>, but I think that wrappers are not a scalable solution. I think a better solution would involve segmenting all of the
Entitiesthat we'd like to mutate, then iterating over them.
Also I know see that this issue is a pretty common one to rust. Perhaps this and this article on shared mutability in Rust could point to a better design.