makapuf
makapuf

Reputation: 1400

How to process every value in a HashMap and optionally reject some?

I would like to process the values from a HashMap one by one, while maybe removing some of them.

For example, I would like to do an equivalent of:

use std::collections::HashMap;

fn example() {
    let mut to_process = HashMap::new();
    to_process.insert(1, true);

    loop {
        // get an arbitrary element
        let ans = to_process.iter().next().clone(); // get an item from the hash
        match ans {
            Some((k, v)) => {
                if condition(&k,&v) {
                    to_process.remove(&k);
                }
            }
            None => break, // work finished
        }
    }
}

But this fails to compile:

error[E0502]: cannot borrow `to_process` as mutable because it is also borrowed as immutable
  --> src/lib.rs:12:17
   |
9  |         let ans = to_process.iter().next().clone();
   |                   ---------- immutable borrow occurs here
...
12 |                 to_process.remove(&k);
   |                 ^^^^^^^^^^^------^^^^
   |                 |          |
   |                 |          immutable borrow later used by call
   |                 mutable borrow occurs here

I know I really would need https://github.com/rust-lang/rust/issues/27804 (which is for HashSet but for HashMap would be the same) and I cannot implement the provided solutions without having a non-mut and mutable reference still or using unsafe.

Is there a simple way I am missing?

Upvotes: 3

Views: 944

Answers (2)

joel
joel

Reputation: 7867

Note If you need to alter keys or add kvps to the HashMap during processing, see @edwardw's answer. Otherwise ...

Use HashMap::retain. You can change your process function to return a bool indicating whether to keep that key value pair. For example

let mut to_process: HashMap<u32, String> = HashMap::new();
to_process.insert(1, "ok".to_string());
to_process.insert(2, "bad".to_string());

to_process.retain(process);
    
fn process(k: &u32, v: &mut String) -> bool {
    // do stuff with k and v
    v == "ok"
}

Upvotes: 5

edwardw
edwardw

Reputation: 13942

This looks like an awfully good fit for Iterator::filter_map:

The closure must return an Option<T>. filter_map creates an iterator which calls this closure on each element. If the closure returns Some(element), then that element is returned. If the closure returns None, it will try again, and call the closure on the next element, seeing if it will return Some.

The following process_and_maybe_add is very simple, but you get the idea:

use std::collections::HashMap;

fn main() {
    let mut data = HashMap::new();
    data.insert(1, "a");
    data.insert(2, "b");
    data.insert(3, "c");

    let processed = data
        .into_iter()
        .filter_map(process_and_maybe_add)
        .collect::<HashMap<_, _>>();
    dbg!(processed);
}

fn process_and_maybe_add((k, v): (u32, &str)) -> Option<(u32, String)> {
    if k % 2 != 0 {
        Some((k + 100, v.to_owned() + v))
    } else {
        None
    }
}

Upvotes: 4

Related Questions