How to borrow mutably from an Option<HashMap>?

I have some code where expensive computations need to be performed lazily. So I have a set of values which are either raw (RawValue) or derived from raw (Value), where derivation can be expensive, and I combine the values in an enum of RawValue and Value, and hold that enum (InternalValue) in an Option<HashMap<String, InternalValue>>. The idea is that when I want to process some value and find that it's raw, I want to use it to derive a value and store it back into the hashmap so it can be used without re-derivation next time. I can't seem to get the exact syntax for this right - the compiler tells me I can't borrow as mutable, but I can't see why not other than that I'm using some wrong approach to do it.

use std::collections::HashMap;

#[derive(Debug, Clone, PartialEq)]
pub enum RawValue {
    TBD,
}

#[derive(Debug, Clone, PartialEq)]
pub enum Value<'a> {
    Integer(i64),
    Sequence(&'a Vec<Value<'a>>),
}

#[derive(Debug, Clone, PartialEq)]
pub enum InternalValue<'a> {
    Base(Value<'a>),
    ToBeDerived(RawValue),
}

#[derive(Debug, Clone, PartialEq)]
pub struct Thing<'a> {
    data: Option<HashMap<String, InternalValue<'a>>>,
}

fn expensive_derivation(_rv: &RawValue) -> InternalValue {
    InternalValue::Base(Value::Integer(-1))
}

impl<'a> Thing<'a> {
    fn process(&mut self, s: &str) {
        assert!(self.data.is_some());

        match self.data.as_ref().as_mut().unwrap().get(s) {
            None => {},
            Some(v) => {
                match v {
                    InternalValue::ToBeDerived(rv) => {
                        // Do something to map rv to a Value (potentially expensive),
                        // then store it back into the hashmap to avoid rederivation
                        let iv = expensive_derivation(&rv);
                        self.data.as_ref().as_mut().unwrap().insert(s.to_string(), iv);
                    },
                    _ => {}
                }
            }
        }
    }
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0596]: cannot borrow data in a `&` reference as mutable
  --> src/lib.rs:41:25
   |
41 |                         self.data.as_ref().as_mut().unwrap().insert(s.to_string(), iv);
   |                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot borrow as mutable

error: aborting due to previous error

For more information about this error, try `rustc --explain E0596`.
error: could not compile `playground`.

To learn more, run the command again with --verbose.

I think (without trying it) that you can make this work by using entry(s) instead of get(s) and then modifying the entry in place?

https://doc.rust-lang.org/std/collections/hash_map/struct.HashMap.html#method.entry

Oh, and if the Option is really getting in your way, do you need it? Is None useful vs just an empty HashMap (which doesn't allocate anything)?

I made it compile:

impl<'a> Thing<'a> {
    fn process(&'a mut self, s: &str) {
        match self.data {
            None => return,
            Some(ref mut hm) => hm.entry(s.to_string()).and_modify(|v| {
                if let InternalValue::ToBeDerived(rv) = v {
                    *v = expensive_derivation(rv);
                }
            }),
        };
    }
}

Playground link

2 Likes

P.S. You can remove ref mut by matching on &mut self.data instead, which might be a bit more readable.

Also, I had to call s.to_string(), since the entry() API doesn't seem to support passing a borrowed key. I'm not sure if/how this can be fixed.

That has been a open problem for some time. https://github.com/rust-lang/rfcs/issues/1203

Thanks, all, for your suggestions. I'm using an Option because of how I return errors in the real application.

AFAICS this doesn't need entry, just get_mut:

impl<'a> Thing<'a> {
    fn process(&'a mut self, s: &str) {
        if let Some(data) = &mut self.data {
            if let Some(v) = data.get_mut(s) {
                if let InternalValue::ToBeDerived(rv) = v {
                    *v = expensive_derivation(rv);
                }
            }
        }
    }
}

Playground

4 Likes

However, I note that it still fails to compile if expensive_derivation is a method rather than a function (mut or not - just a slightly different error message):

impl<'a> Thing<'a> {
    fn expensive_derivation(&'a self, _rv: &RawValue) -> InternalValue<'a> {
        InternalValue::Base(Value::Integer(-1))
    }
    
    fn process(&'a mut self, s: &str) {
        if let Some(data) = &mut self.data {
            if let Some(v) = data.get_mut(s) {
                if let InternalValue::ToBeDerived(rv) = v {
                    *v = self.expensive_derivation(rv);
                }
            }
        }
    }
}

(Playground)

error[E0502]: cannot borrow `*self` as immutable because it is also borrowed as mutable
  --> src/lib.rs:34:26
   |
31 |         if let Some(data) = &mut self.data {
   |                             -------------- mutable borrow occurs here
...
34 |                     *v = self.expensive_derivation(rv);
   |                     -----^^^^-------------------------
   |                     |    |
   |                     |    immutable borrow occurs here
   |                     mutable borrow later used here

error: aborting due to previous error

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.