More concise HashMap initialization

RustHashmap

Rust Problem Overview


I'm using a HashMap to count the occurrences of different characters in a string:

let text = "GATTACA";
let mut counts: HashMap<char, i32> = HashMap::new();
counts.insert('A', 0);
counts.insert('C', 0);
counts.insert('G', 0);
counts.insert('T', 0);

for c in text.chars() {
    match counts.get_mut(&c) {
        Some(x) => *x += 1,
        None => (),
    }
}

Is there a more concise or declarative way to initialize a HashMap? For example in Python I would do:

counts = { 'A': 0, 'C': 0, 'G': 0, 'T': 0 }

or

counts = { key: 0 for key in 'ACGT' }

Rust Solutions


Solution 1 - Rust

You can use iterators to emulate the dictionary comprehension, e.g.

let counts = "ACGT".chars().map(|c| (c, 0_i32)).collect::<HashMap<_, _>>();

or even for c in "ACGT".chars() { counts.insert(c, 0) }.

Also, one can write a macro to allow for concise initialisation of arbitrary values.

macro_rules! hashmap {
    ($( $key: expr => $val: expr ),*) => {{
         let mut map = ::std::collections::HashMap::new();
         $( map.insert($key, $val); )*
         map
    }}
}

used like let counts = hashmap!['A' => 0, 'C' => 0, 'G' => 0, 'T' => 0];.

Solution 2 - Rust

Another way that I see in the official documentation:

use std::collections::HashMap;

fn main() {
    let timber_resources: HashMap<&str, i32> =
    [("Norway", 100),
     ("Denmark", 50),
     ("Iceland", 10)]
     .iter().cloned().collect();
    // use the values stored in map
}

EDIT

When I visit the official doc again, I see that the sample is updated (and the old sample is removed). So here is the latest solution with Rust 1.56:

let vikings = HashMap::from([
    ("Norway", 25),
    ("Denmark", 24),
    ("Iceland", 12),
]);

Solution 3 - Rust

Starting with Rust 1.56, you can use from() to build a Hashmap from an array of key-value pairs. This makes it possible to initialize concisely without needing to specify types or write macros.

use std::collections::HashMap;

fn main() {
    let m = HashMap::from([
        ('A', 0),
        ('C', 0),
        ('G', 0),
        ('T', 0)
    ]);
}

Solution 4 - Rust

This (very common) scenario is why I heard angels singing when I discovered Python's defaultdict, a dictionary which, if you try to get a key that isn't in the dictionary, immediately creates a default value for that key with a constructor you supply when you declare the defaultdict. So, in Python, you can do things like:

counts = defaultdict(lambda: 0)
counts['A'] = counts['A'] + 1

For counting occurrences, this is the favored approach since trying to pre-populate the hashtable becomes problematic when the keyspace is either large or unknown to the programmer (Imagine something which counts words in text you feed to it. Are you going to pre-populate with all English words? What if a new word enters the lexicon?).

You can achieve this same thing in Rust with the lesser-known methods in the Option class. Seriously, when you have some free time, just read through all of the methods in Option. There are some very handy methods in there.

Although not dealing with concise initialization (which is what the wubject is asking for) here are two answers (which are, arguably, better for doing what OP is trying to do).

let text = "GATTACA";
let mut counts:HashMap<char,i32> = HashMap::new();
for c in text.chars() {
    counts.insert(c,*(counts.get(&c).get_or_insert(&0))+1);
}

The above method uses Option's get or insert() method which, if it's a Some(), returns the value and, if a None, returns a value you provide. Note that, even though the method is named get_or_insert(), it is not inserting into the hashmap; this is a method for Option and the hashmap has no idea this fail-over is taking place. The nice bit is that this unwraps the value for you. This is pretty similar to Python's defaultdict, with the difference that you have to provide a default value in multiple locations in your code (inviting bugs, but also providing an added flexibility that defaultdict lacks).

let text = "GATTACA";
let mut counts:HashMap<char,i32> = HashMap::new();
for c in text.chars() {
    counts.insert(c,counts.get(&c).or_else(|| Some(&0)).unwrap()+1);
}

This approach uses Option's or else() method which lets you specify a lambda for producing the value and, crucially, lets you still return a None (imagine if you wanted to check a hashmap for a key and, if not found, check another hashmap for it, and, only if not found in either, did you produce a None). Because or else() returns an option, we must use unwrap() (which would panic if used on a None, but we know that won't apply here).

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionanderspitmanView Question on Stackoverflow
Solution 1 - RusthuonView Answer on Stackoverflow
Solution 2 - Rustch271828nView Answer on Stackoverflow
Solution 3 - RustDaniel GigerView Answer on Stackoverflow
Solution 4 - RustJemenakeView Answer on Stackoverflow