2
votes

I have a data structure like this:

struct R {
    hmhs: HashMap<i64, HashSet<i64>>,
}

impl R {
    fn hs_for_hmhs(&mut self) -> &mut HashSet<i64> {
        if let None = self.hmhs.get(&0) {
            self.hmhs.insert(0, HashSet::new());
        }

        self.hmhs.get_mut(&0).unwrap()
    }

    fn iter_for_hmhs<'a>(&'a mut self) -> impl Iterator<Item = &'a i64> {
        self.hs_for_hmhs().iter()
    }

    fn insert_for_hmhs(&mut self, i: i64) -> bool {
        self.hs_for_hmhs().insert(i)
    }
}

This seems to work, but all the methods require a mutable reference to self which is unfortunate. I tried to give interior mutability a go:

struct S {
    hmhs: RefCell<HashMap<i64, HashSet<i64>>>,
}

impl S {
    fn hs_for_hmhs(&self) -> &HashSet<i64> {
        if let None = self.hmhs.borrow().get(&0) {
            self.hmhs.borrow_mut().insert(0, HashSet::new());
        }

        self.hmhs.borrow_mut().get_mut(&0).unwrap()
    }

    fn iter_for_hmhs(&mut self) -> impl Iterator<Item = &i64> {
        self.hs_for_hmhs().iter()
    }

    fn insert_for_hmhs(&mut self, i: i64) -> bool {
        self.hs_for_hmhs().insert(i)
    }
}

However, I constantly seem to hit problems. Mostly some variety of How do I return a reference to something inside a RefCell without breaking encapsulation?

I have tried lots of variants here, but I am missing something fundamental in my understanding. Is there a way of achieving what I want?

Complete Code:

use std::cell::RefCell;
use std::collections::{HashMap, HashSet};

struct R {
    hmhs: HashMap<i64, HashSet<i64>>,
}

impl R {
    fn hs_for_hmhs(&mut self) -> &mut HashSet<i64> {
        if let None = self.hmhs.get(&0) {
            self.hmhs.insert(0, HashSet::new());
        }

        self.hmhs.get_mut(&0).unwrap()
    }

    fn iter_for_hmhs<'a>(&'a mut self) -> impl Iterator<Item = &'a i64> {
        self.hs_for_hmhs().iter()
    }

    fn insert_for_hmhs(&mut self, i: i64) -> bool {
        self.hs_for_hmhs().insert(i)
    }
}

struct S {
    hmhs: RefCell<HashMap<i64, HashSet<i64>>>,
}

impl S {
    fn hs_for_hmhs(&self) -> &mut HashSet<i64> {
        if let None = self.hmhs.borrow().get(&0) {
            self.hmhs.borrow_mut().insert(0, HashSet::new());
        }

        self.hmhs.borrow_mut().get_mut(&0).unwrap()
    }

    fn iter_for_hmhs(&self) -> impl Iterator<Item = &i64> {
        self.hs_for_hmhs().iter()
    }

    fn insert_for_hmhs(&self, i: i64) -> bool {
        self.hs_for_hmhs().insert(i)
    }
}

fn main() {}

Compiler Message:

error[E0597]: borrowed value does not live long enough
  --> src/main.rs:36:9
   |
36 |         self.hmhs.borrow_mut().get_mut(&0).unwrap()
   |         ^^^^^^^^^^^^^^^^^^^^^^ temporary value does not live long enough
37 |     }
   |     - temporary value only lives until here
   |
note: borrowed value must be valid for the anonymous lifetime #1 defined on the method body at 31:5...
  --> src/main.rs:31:5
   |
31 | /     fn hs_for_hmhs(&self) -> &mut HashSet<i64> {
32 | |         if let None = self.hmhs.borrow().get(&0) {
33 | |             self.hmhs.borrow_mut().insert(0, HashSet::new());
34 | |         }
35 | |
36 | |         self.hmhs.borrow_mut().get_mut(&0).unwrap()
37 | |     }
   | |_____^
1
Why do you need to borrow it mutably? All of the methods return immutable references to its contents? - Peter Hall
Which? The HashMap and the second borrow_mut call. I don't know. It complains more if I don't. - Phil Lord
Ah, I didn't notice that insert_for_hmhs calls insert. - Peter Hall
Oh, the HashSet -- yes, this part will fulfil the public interface of the struct. The HashMap is an implementation detail really. - Phil Lord
This is a tricky problem, and I wonder if you should try a different approach. For example, this will get you half way - an abstracted smart pointer to the HashSet. But it's difficult to return an iterator to that. - Peter Hall

1 Answers

1
votes

I found a solution -- extract the HashMap as a raw pointer. This in turn means that I can get to the HashSet without shenanigans including returning a iterator.

I'm happy enough with this as a solution. The unsafe code is small and contained and if I understand the reason why the compiler is complaining without unsafe, it cannot occur in this code, since neither the HashMap nor the HashSet are ever removed or replaced after construction.

That was a lot of effort.

use std::cell::RefCell;
use std::collections::{HashMap, HashSet};

struct R {
    hmhs: HashMap<i64, HashSet<i64>>,
}

impl R {
    fn hs_for_hmhs(&mut self) -> &mut HashSet<i64> {
        if let None = self.hmhs.get(&0) {
            self.hmhs.insert(0, HashSet::new());
        }

        self.hmhs.get_mut(&0).unwrap()
    }

    fn iter_for_hmhs<'a>(&'a mut self) -> impl Iterator<Item = &'a i64> {
        self.hs_for_hmhs().iter()
    }

    fn insert_for_hmhs(&mut self, i: i64) -> bool {
        self.hs_for_hmhs().insert(i)
    }
}

struct S {
    hmhs: RefCell<HashMap<i64, HashSet<i64>>>,
}

impl S {
    fn hs_as_ptr(&self) -> *mut HashMap<i64, HashSet<i64>> {
        self.hmhs.borrow_mut().entry(0).or_insert(HashSet::new());
        self.hmhs.as_ptr()
    }

    fn mut_hs_for_hmhs(&mut self) -> &mut HashSet<i64> {
        unsafe { (*self.hs_as_ptr()).get_mut(&0).unwrap() }
    }
    fn hs_for_hmhs(&self) -> &HashSet<i64> {
        unsafe { (*self.hs_as_ptr()).get(&0).unwrap() }
    }

    fn iter_for_hmhs<'a>(&'a self) -> impl Iterator<Item = &'a i64> + 'a {
        self.hs_for_hmhs().iter()
    }

    fn insert_for_hmhs(&mut self, i: i64) -> bool {
        self.mut_hs_for_hmhs().insert(i)
    }
}

fn main() {
    let mut r = R {
        hmhs: HashMap::new(),
    };
    let mut s = S {
        hmhs: RefCell::new(HashMap::new()),
    };

    r.insert_for_hmhs(10);
    s.insert_for_hmhs(20);

    println!("r next: {:?}", r.iter_for_hmhs().next());
    println!("s next: {:?}", s.iter_for_hmhs().next());
}

https://play.rust-lang.org/?gist=3ed1977bdd5f9f82d144fe128f618979&version=stable&mode=debug&edition=2015