How to invoke a function on struct and conditionally invoke another one?

Hello everyone,

I am just trying to get into systems programming/rust, and was trying to write some code similar to this

use rand::Rng;
use std::collections::HashMap;
use std::fmt;


#[derive(Debug,Clone)]
pub struct UserNotFoundError;

impl fmt::Display for UserNotFoundError{
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result{
        write!(f, "USer not found error")
    }
}

struct UserRecord{
    user_id: u32,
    user_name: String,
    password: String,
    age: u8
}

struct UserGroup{
    users: HashMap<u32, UserRecord>,
    group_id: u32,
    group_name: String
}

impl UserGroup{
    pub fn new(group_name: String, group_id: u32) -> Self{
        return UserGroup{users: HashMap::new(),group_id, group_name};
    }
    
    fn load_user_from_disk(&mut self, user_id: u32){
        let curr_user = UserRecord{user_id: user_id, user_name: String::from("Random_String"), password: String::from("random_password"), age: 12};
        self.users.insert(user_id, curr_user);
        
    }
    
    pub fn get_or_add_user(&mut self, user_id: u32) -> Result<&UserRecord,UserNotFoundError>{
        if let Some(user_record) = self.users.get(&user_id){
            return Ok(user_record);
        }else{
            self.load_user_from_disk(user_id);
            if let Some(user_record) = self.users.get(&user_id){
                return Ok(user_record);
            }else{
                return Err(UserNotFoundError)
            }   
        }
        
        
    }
}


fn main() {
    let mut curr_user_group = UserGroup::new(String::from("random_group"), 12312312);
    let mut rng = rand::thread_rng();

    let new_user_id: u32 = rng.gen();
    curr_user_group.get_or_add_user(new_user_id);
}

Playground link

Essentially what I am trying to do is:

  1. I want to expose a struct method that hands out mutable references to another struct stored in a hash map.
  2. Incase the required struct is not there, we load it from the disk/db, set it in the hashmap and then return the reference to the struct.

However I get the error

 Compiling playground v0.0.1 (/playground)
error[E0502]: cannot borrow `*self` as mutable because it is also borrowed as immutable
  --> src/main.rs:43:13
   |
39 |     pub fn get_or_add_user(&mut self, user_id: u32) -> Result<&UserRecord,UserNotFoundError>{
   |                            - let's call the lifetime of this reference `'1`
40 |         if let Some(user_record) = self.users.get(&user_id){
   |                                    ------------------------ immutable borrow occurs here
41 |             return Ok(user_record);
   |                    --------------- returning this value requires that `self.users` is borrowed for `'1`
42 |         }else{
43 |             self.load_user_from_disk(user_id);
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `playground` (bin "playground") due to previous error

I am not able to figure out how to solve this. I did see a bunch of questions with varied answers:

  1. Introduce a separate block for the code that checks if the key exists in the hashmap or not. - Tried, did not work.

  2. Split the functionality - I am not sure how I can split this up? This is fairly normal is high level languages. If we can't do this here, then can someone please explain why and what's a good way to think about this ? I just want an implementation that hides the complexity as such.

I did go over the answer here -> reference - Cannot borrow as mutable because it is also borrowed as immutable - Stack Overflow

Is cloning or using RC/ARC the actual idiomatic way to do something like this in Rust or more like a bandaid over bad design(In which case what's good way to describe/expose something like this?)

This is what HashMap::entry() is for:

pub fn get_or_add_user(&mut self, user_id: u32) -> Result<&UserRecord, UserNotFoundError> {
    Ok(self.users.entry(user_id).or_insert_with(|| UserRecord {
        user_id: user_id,
        user_name: String::from("Random_String"),
        password: String::from("random_password"),
        age: 12,
    }))
}

When the lookup starts to be able to fail in real code, switch from .or_insert_with() to matching the Entry so you can decide to not insert if the disk has no record.

2 Likes

You're running into an error that really shouldn't exist, but rusts borrow checker isn't smart enough for it (yet).

It pessimistically assumes that the borrow for .get(…) needs to outlast the function call as its result is returned from the function, even though this return only happens conditionally, and the borrow could safely be considered to be a lot shorter in the other non-returning case.

One possible workaround for this is to do another call to get once you're certain that you will return, e. g. by replacing the first

return Ok(user_record);

with

return Ok(self.users.get(&user_id).unwrap());

Rust Playground

This change is enough to make the borrow checker understand everything is fine; the downside being that the additional call to get does do a little bit of redundant work.


The .entry API @kpreid shows above is another (often cleaner) way to work around this kind of issue, though I don't think you can use it here without also somehow adjusting the signature of, or perhaps inlining, the load_user_from_disk method.

load_user_from_disk should return a Result<UserRecord> and then this works