84

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' }
nbro
  • 15,395
  • 32
  • 113
  • 196
anderspitman
  • 9,230
  • 10
  • 40
  • 61

4 Answers4

105

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),
]);
ch271828n
  • 15,854
  • 5
  • 53
  • 88
  • Why not `into_iter`? – Tommaso Thea Jun 01 '21 at 22:42
  • @TommasoTheaCioni good question, did you try it? – ch271828n Jun 01 '21 at 23:17
  • 1
    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. – Tommaso Thea Jun 03 '21 at 18:35
  • 2
    @TommasoTheaCioni The good news is, in edition 2021 `into_iter()` works on arrays [as expected](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=86155b0b2fc7c09762e344b59e3a664b)! – user4815162342 Sep 02 '21 at 15:00
  • 1
    See @Daniel-Giger answer: you need Rust version 1.56. – Ted Nov 26 '21 at 00:13
  • 1
    @Ted thanks I have updated explanations, such that people with >=1.56 and <1.56 can both see what to do – ch271828n Nov 26 '21 at 02:30
  • 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* – CrepeGoat Dec 18 '21 at 03:14
101

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];.

huon
  • 94,605
  • 21
  • 231
  • 225
20

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)
    ]);
}
Daniel Giger
  • 2,023
  • 21
  • 20
  • 1
    Thanks for pointing out the version number. I've been using 1.54 and couldn't get HashMap::from() to work. – Ted Nov 26 '21 at 00:08
2

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

Jemenake
  • 2,092
  • 1
  • 20
  • 16
  • 2
    I would say that using the [HashMap Entry API](https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.entry) is better. – Kushagra Gupta Jun 22 '20 at 03:33
  • @KushagraGupta is right that entry() is better, and (almost) this use-case is featured in chapter 8 of of [The Rust Programming Language](https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html). – Deebster Sep 28 '20 at 11:40