More concise HashMap initialization

21,046

Solution 1

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

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

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

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).

Share:
21,046
anderspitman
Author by

anderspitman

I'm a software architect who's passionate about improving human health.

Updated on July 05, 2022

Comments

  • anderspitman
    anderspitman about 2 years

    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' }
    
  • Piotr Zolnierek
    Piotr Zolnierek over 9 years
    THAT should definitely be in std!
  • ch271828n
    ch271828n almost 5 years
    Excellent and give me (newlearner) an example of macro! Thanks!
  • ch271828n
    ch271828n over 4 years
    @PiotrZolnierek That is actually in std now! (see my answer :) )
  • Kushagra Gupta
    Kushagra Gupta about 4 years
    I would say that using the HashMap Entry API is better.
  • Deebster
    Deebster almost 4 years
    @KushagraGupta is right that entry() is better, and (almost) this use-case is featured in chapter 8 of of The Rust Programming Language.
  • Tommaso Thea Cioni
    Tommaso Thea Cioni about 3 years
    Why not into_iter?
  • ch271828n
    ch271828n about 3 years
    @TommasoTheaCioni good question, did you try it?
  • Tommaso Thea Cioni
    Tommaso Thea Cioni about 3 years
    Yeah I just did, it doesn't work. I realized it has to do with the limitations of into_iter on arrays. It still gives you references, just like iter, so no point in updating the answer.
  • user4815162342
    user4815162342 almost 3 years
    @TommasoTheaCioni The good news is, in edition 2021 into_iter() works on arrays as expected!
  • Ted
    Ted over 2 years
    Thanks for pointing out the version number. I've been using 1.54 and couldn't get HashMap::from() to work.
  • Ted
    Ted over 2 years
    See @Daniel-Giger answer: you need Rust version 1.56.
  • ch271828n
    ch271828n over 2 years
    @Ted thanks I have updated explanations, such that people with >=1.56 and <1.56 can both see what to do
  • CrepeGoat
    CrepeGoat over 2 years
    for those using older rust versions (e.g., 1.55 for me): even though array.into_iter() makes an iterator over item references for primitive arrays, I think IntoIterator::into_iter(array) properly iterates over item values