1

I'm trying to implement Dijkstra's algorithm in Rust. I am getting an error because I'm mutating a HashMap inside a match expression on a value obtained from .get(). I understand why this is a violation of the borrowing and mutation principles of Rust, but I haven't found a workaround.

I have tried using .entry() and .and_modify() to perform an in-place modification, but I also need to insert and/or modify other keys besides the one being matched on.

Are there any constructs/functions I can use to safely enable this mutation within a get, or, failing that, is there another approach to this algorithm that steers clear of this issue?

(Unfinished) Code Snippet:

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

struct Graph {
    vertices: Vec<i32>,
    edges: HashMap<i32, HashMap<i32, i32>>, // start -> {end -> weight}
}

fn dijkstra(start: i32, g: Graph) -> HashMap<i32, HashMap<i32, (i32, Vec<i32>)>> {
    let mut known_paths: HashMap<i32, (i32, Vec<i32>)> = HashMap::new(); // end -> (total_weight, path)

    known_paths.insert(start, (0, Vec::new()));

    let mut current = &start;
    let mut unvisited: HashSet<i32> = g.edges.keys().cloned().collect();

    while unvisited.len() > 0 {
        match known_paths.get(current) {
            Some((current_dist, current_path)) => if let Some(ref incident_edges) =
                g.edges.get(current)
            {
                for (neighbor, dist) in incident_edges.iter() {
                    match known_paths.get(&neighbor) {
                        Some((old_distance, _old_path)) => if current_dist + dist < *old_distance {
                            let mut new_path = current_path.clone();
                            new_path.push(*current);
                            known_paths.insert(*neighbor, (current_dist + dist, new_path));
                        },
                        None => {
                            let mut new_path = current_path.clone();
                            new_path.push(*current);
                            known_paths.insert(*neighbor, (current_dist + dist, new_path));
                        }
                    }
                }
            },
            None => panic!("Something went wrong with current={}", current),
        }
    }
    HashMap::new()
}

Error:

error[E0502]: cannot borrow `known_paths` as mutable because it is also borrowed as immutable
  --> src/lib.rs:26:29
   |
17 |         match known_paths.get(current) {
   |               ----------- immutable borrow occurs here
...
26 |                             known_paths.insert(*neighbor, (current_dist + dist, new_path));
   |                             ^^^^^^^^^^^ mutable borrow occurs here
...
37 |         }
   |         - immutable borrow ends here

error[E0502]: cannot borrow `known_paths` as mutable because it is also borrowed as immutable
  --> src/lib.rs:31:29
   |
17 |         match known_paths.get(current) {
   |               ----------- immutable borrow occurs here
...
31 |                             known_paths.insert(*neighbor, (current_dist + dist, new_path));
   |                             ^^^^^^^^^^^ mutable borrow occurs here
...
37 |         }
   |         - immutable borrow ends here
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
BowlingHawk95
  • 1,518
  • 10
  • 15
  • 1
    What is your *question*? – Shepmaster Aug 24 '18 at 02:16
  • See also [Which algorithm from petgraph will find the shortest path from A to B?](https://stackoverflow.com/q/43420605/155423); [How can I simultaneously iterate over a Rust HashMap and modify some of its values?](https://stackoverflow.com/q/47737084/155423); [cannot borrow variable as mutable because it is also borrowed as immutable while building a self-referential HashMap](https://stackoverflow.com/q/35524499/155423); [HashMap: borrow read/write at the same time](https://stackoverflow.com/q/44453398/155423); etc. – Shepmaster Aug 24 '18 at 02:44
  • @Shepmaster forgive me, I assumed the question would be implicit given the error and my statement of the desired algorithm. Edited. Thank you for the alternate questions, none of these came up in my Google searches for this problem. I will look them over and see what I can take away from them. – BowlingHawk95 Aug 24 '18 at 03:32
  • @Shepmaster after reading the above answers, I see that `RefCell` might be able to help, but I have questions. 1) `RefCell` seems to allow multiple mutable references to one value in a map, but does that help with my needs to mutate multiple key-value pairs inside a borrow? 2) It seems that `RefCell` ought to be avoided (or at least minimized) for philosophical reasons, since it side-steps Rust's compile-time borrow checking. If that's the case, then is there a good alternate approach to the algorithm above that gets away from this issue altogether? – BowlingHawk95 Aug 24 '18 at 05:11

1 Answers1

2

No.

I am getting an error because I'm mutating a HashMap inside a match expression on a value obtained from .get(). I understand why this is a violation of the borrowing and mutation principles of Rust, but I haven't found a workaround.

I am afraid you have not actually understood the borrowing principles.

The key principle which underpins Rust's safety is: Mutation NAND Aliasing.

This principle is then enforced either at run-time or at compile-time, with a strong preference for compile-time when feasible since it is free of run-time overhead.

In your situation, you have:

  1. A reference inside HashMap, obtained from HashMap::get().
  2. You attempt to modify said HashMap while holding onto the reference.

Let's imagine, for a second, that some constructs allows this code to compile and run; what would happen? BOOM.

When inserting an element in the HashMap, the HashMap may:

  1. Shuffle existing elements, since it is using Robin Hood hashing.
  2. Transfer all elements to another (larger) heap-allocated array.

Therefore, HashMap::insert means that any existing reference to an element of the HashMap may become dangling and point to either another element, or random memory.

There is no safe way to insert into the HashMap while holding a reference to an element of the HashMap.


You need to find a solution which does not involve keeping a reference to the HashMap.

My advice would be to simply clone: match known_paths.get(current).clone(). Once you have a first working implementation of the algorithm, you can look into possibly improving its performance, as necessity dictates.

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • [Possible solution](https://github.com/bluss/petgraph/blob/master/src/dijkstra.rs#L21) – Stargateur Aug 24 '18 at 11:28
  • Excellent, informative answer. I hadn't thought about Rust shuffling or cloning the `HashMap` out from under me, which of course is obvious now that I think about it. Your suggestion to clone the get result sounds straightforward and plausible, let me play with it and get back to you. Thanks! – BowlingHawk95 Aug 24 '18 at 15:12
  • @Stargateur Thanks, perhaps I will look at someone else's implementation for comparison once I finish my own. I am using this partially as a learning experience for Rust, so looking at another implementation without struggling through my own rather defeats the point. – BowlingHawk95 Aug 24 '18 at 15:13