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?

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. Add HashMap.entry_or_clone() method · Issue #1203 · rust-lang/rfcs · GitHub

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.