7

Is there a simple way to fill a 2 dimensional HashMap? In C++ I could do something along the lines of:

std::unordered_map<int,std::unordered_map<int,int>> 2dmap;
//...
2dmap[0][0] = 0;
2dmap[0][1] = 1;
//...

In my Rust project I am trying to fill a similar map:

let mut map: HashMap<i32, HashMap<i32, i32>> = HashMap::new();
//...fill the hash map here.

The only way that I can think to do this would be to build each sub map then move them into the super map, something like this:

let mut sub1: HashMap<i32, i32> = HashMap::new();
sub1.insert(0, 0);
sub1.insert(1, 1);
let mut map: HashMap<i32, HashMap<i32, i32>>::new();
map.insert(0, sub1);

Is there a more concise way to do this?

The above code is a simplification of my actual use case which uses an enum as the index of the HashMap:

enum Example {
    Variant1,
    Variant2,
    //...
}

None of the variants hold a value. I am using this syntax for the lookup from my HashMap:

let value = map[Example::Variant1][Example::Variant2];
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Alex Zywicki
  • 2,263
  • 1
  • 19
  • 34
  • 3
    Since tuples work so well in rust, why not use `HashMap<(i32,i32),i32>`? – Alec Mar 23 '17 at 18:14
  • After your edit, you can still use `HashMap<(example, example),i32>`. Do you really ever need to do a lookup with only the first `example`? If not, you should be using tuples... – Alec Mar 23 '17 at 19:04
  • @Alec is the argument that using tuples would provide a more compact representation? or faster lookup? both? what is the rationale? I am not opposed to the idea, I just want to understand the reasoning rather than just the solution. – Alex Zywicki Mar 23 '17 at 19:12
  • 1
    The argument is more about the semantics. Incidentally, the representation _will_ be more compact and faster. There will be no overhead for extra `HashMap`s each allocating more memory than they need and only one lookup in a table is needed instead of two. – Alec Mar 23 '17 at 19:28

3 Answers3

11

In Rust, tuples work really well. You probably should just use HashMap<(i32, i32), i32>. Then, you end up with something very close to the C++ code.

let mut map: HashMap<(i32, i32), i32> = HashMap::new();
sub1.insert((0, 0), 0);
sub1.insert((0, 1), 1);
// ...

Naturally, it would be nice if we had a macro like vec! for this, and an RFC is in the works. Using the macro from this answer, you can write:

let map = hashmap![(0,0) => 0, (0,1) => 1, ...];

Nothing changes if you are using an enum - just make sure you derive Eq, PartialEq, and Hash for it.

let mut sub1: HashMap<(Example, Example), i32> = HashMap::new();
sub1.insert((Example::Variant1, Example::Variant1), 0);
sub1.insert((Example::Variant1, Example::Variant2), 1);
// ...
Community
  • 1
  • 1
Alec
  • 31,829
  • 7
  • 67
  • 114
  • What would the lookup syntax be? – Alex Zywicki Mar 23 '17 at 18:22
  • 1
    @AlexZywicki `map.get(&(0,1))`? – Alec Mar 23 '17 at 18:28
  • There's no reason to specify the type of the `HashMap`, type inference will handle it. – Shepmaster Mar 23 '17 at 19:56
  • 1
    @Shepmaster Oh sure. But, as someone who has worked for most of my programming life in languages that support type inference (Haskell and Scala), I still like to add annotations at definition sites where the type isn't _immediately_ obvious. Future-me ends up being happier with present-me. – Alec Mar 23 '17 at 19:59
  • @AlexZywicki Note that if you want to _unsafely_ get a value from the map, you can do this too: `map[&(0,1)]`. – Alec Mar 23 '17 at 21:49
  • 2
    @Alec The word "unsafe" has a specific meaning in Rust. `map[&(0, 1)]` is safe (in the Rust sense), but it can panic. – user4815162342 Mar 23 '17 at 22:22
7

Use the entry API:

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.entry(0).or_insert_with(HashMap::new).insert(0, 0);
    map.entry(0).or_insert_with(HashMap::new).insert(1, 1);

    println!("{:?}", map);
    println!("{}", map[&0][&1]);
}

Unlike C++, constructing the nested HashMap is not implicit - you have to be very clear that you wish to allow a new HashMap to be created.


Unlike the other answer, this preserves the original data structure and ability to get a subset of the overall map based on just the initial key:

println!("{:?}", map.get(&0));
println!("{:?}", map.get(&0).and_then(|m| m.get(&1)));

If you always provide both numeric indices, then a tuple is a much better solution as it more accurately models the problem — there's only one true key, it just happens to have two components. It also is likely to have better data locality as there's one big slab of memory, as opposed to the extra level of pointer-chasing a hash of hashes would entail.

Community
  • 1
  • 1
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
2

The C++ code snippet owes its brevity to the convenient operator [] which automatically default-constructs the value if it is missing in the map. While Rust doesn't do this by default, it's easy enough to tell it to do so, as shown in Shepmaster's answer.

To avoid having to spell out entry(key).or_insert_with(ValueType::new), it is possible to add the method equivalent to C++'s operator [] to Rust's HashMap. After all, Rust has the necessary tools - it supports adding methods to existing types using traits, and it has a trait roughly equivalent to C++'s default constructor.

The C++ expressions:

map[0][0] = 0;
map[0][1] = 1;

would be written as the following Rust, using a method that returns a reference in place of operator []:

*map.ensure(0).ensure(0) = 0;
*map.ensure(0).ensure(1) = 1;

ensure would be declared in a trait which must be imported by code that wants to get the method:

use std::collections::HashMap;
use std::hash::Hash;

trait MapDefault<K, V: Default> {
    fn ensure(&mut self, key: K) -> &mut V;
}

...and defined like this:

impl<K: Eq + Hash, V: Default> MapDefault<K, V> for HashMap<K, V> {
    fn ensure(&mut self, key: K) -> &mut V {
        self.entry(key).or_insert_with(V::default)
    }
}

It would be nice to be able to define IndexMut for HashMap, which would shorten the expression to *map[0][0] = 0, which is almost exactly like the C++ original, but unfortunately Rust doesn't allow implementing operators for types from other modules.

user4815162342
  • 141,790
  • 18
  • 296
  • 355
  • You could implement a wrapper type that does implement `IndexMut` though? – Shepmaster Mar 23 '17 at 23:46
  • 1
    Actually, maybe not. [There's a reason that `HashMap` doesn't implement `IndexMut`](http://stackoverflow.com/a/30414450/155423), so you can't really do it. The method is the better choice for now. – Shepmaster Mar 24 '17 at 03:00
  • @Shepmaster Thanks for the hint, I'll try to experiment with implementing `IndexMut::get_mut` similar to `ensure` on the wrapper type. `HashMap` itself cannot implement the kind of `IndexMut` presented in the answer because its values do not require the `Default` trait (nor should they, IMHO - the feature is not worth it). The `IndexMut` that was previously present and removed would simply [*panic*](https://github.com/rust-lang/rust/pull/23559/files#diff-2315431fee3b24e47693061ba8319e44L928) on a non-existent key, which is definitely not the semantics programmers expect of `map[k] = v`. – user4815162342 Mar 24 '17 at 07:24