35

Being fairly new to Rust, I was wondering on how to create a HashMap with a default value for a key? For example, having a default value 0 for any key inserted in the HashMap.

In Rust, I know this creates an empty HashMap:

let mut mymap: HashMap<char, usize> = HashMap::new();

I am looking to maintain a counter for a set of keys, for which one way to go about it seems to be:

for ch in "AABCCDDD".chars() {
    mymap.insert(ch, 0)
}

Is there a way to do it in a much better way in Rust, maybe something equivalent to what Ruby provides:

mymap = Hash.new(0)
mymap["b"] = 1
mymap["a"] # 0
hindenbug
  • 549
  • 1
  • 8
  • 15
  • 2
    Unless I'm missing something, it seems that you're laboring under an invalid assumption. Zero is a valid key, so setting the key to zero by default doesn't make any sense to me. What is the use case for this? – Robert Harvey Jan 01 '17 at 18:03
  • 1
    @RobertHarvey My bad, reworded the last line, I am looking for a way to setup a default value for any key added to the HashMap? Ex: { "A" => 0 } – hindenbug Jan 01 '17 at 18:12
  • You keep talking about when you *insert* a key, but it you are inserting a key, you can just type `0` as the value argument. I concur with @RobertHarvey; what are you trying to do? I'd suggest [edit]ing your question to show example (pseudo)code of what you'd be able to do if such a default exists. – Shepmaster Jan 01 '17 at 18:21
  • 2
    I don't know about Ruby, but in C++ there's a notion of a map *inserting* a *default* value when a key is accessed (cf. http://en.cppreference.com/w/cpp/container/map/operator_at). That always seemed a bit leaky though: what if the type doesn't have a default? Rust is less demanding on the mapped types and more explicit about the presence (or absence) of a key. – ArtemGr Jan 01 '17 at 19:04
  • 1
    @ArtemGr that's also interesting in Rust as it means that a `get` would need to mutate the hashmap. – Shepmaster Jan 01 '17 at 19:09

4 Answers4

52

Answering the problem you have...

I am looking to maintain a counter for a set of keys.

Then you want to look at How to lookup from and insert into a HashMap efficiently?. Hint: *map.entry(key).or_insert(0) += 1


Answering the question you asked...

How does one create a HashMap with a default value in Rust?

No, HashMaps do not have a place to store a default. Doing so would cause every user of that data structure to allocate space to store it, which would be a waste. You'd also have to handle the case where there is no appropriate default, or when a default cannot be easily created.

Instead, you can look up a value using HashMap::get and provide a default if it's missing using Option::unwrap_or:

use std::collections::HashMap;

fn main() {
    let mut map: HashMap<char, usize> = HashMap::new();
    map.insert('a', 42);

    let a = map.get(&'a').cloned().unwrap_or(0);
    let b = map.get(&'b').cloned().unwrap_or(0);

    println!("{}, {}", a, b); // 42, 0
}

If unwrap_or doesn't work for your case, there are several similar functions that might:

Of course, you are welcome to wrap this in a function or a data structure to provide a nicer API.


ArtemGr brings up an interesting point:

in C++ there's a notion of a map inserting a default value when a key is accessed. That always seemed a bit leaky though: what if the type doesn't have a default? Rust is less demanding on the mapped types and more explicit about the presence (or absence) of a key.

Rust adds an additional wrinkle to this. Actually inserting a value would require that simply getting a value can also change the HashMap. This would invalidate any existing references to values in the HashMap, as a reallocation might be required. Thus you'd no longer be able to get references to two values at the same time! That would be very restrictive.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • 7
    This is an interesting answer, as the reasons for NOT having a default value can be important, but I think this answer would benefit a lot from explaining how to solve the problem the OP is having *first*. I have many times used the `defaultdict` of Python, and I can see how having a "short recipe" on how to achieve this in Rust would be valuable... which your answer does not, actually, provide directly. – Matthieu M. Jan 02 '17 at 10:40
  • 1
    @MatthieuM. that's a thing I struggle with. I tend to optimize answers for the *question being asked* as I want to cater to the people that find a question through search. I feel bad when OP asks [an XY problem](http://meta.stackexchange.com/questions/66377/what-is-the-xy-problem), so sometimes I'll answer the bigger question as well. The "directly" part is because the question has already been asked before, thus my link. If you'd like, go ahead and mark it a duplicate of http://stackoverflow.com/a/36154859/155423. – Shepmaster Jan 02 '17 at 15:00
  • This is the kind of little convenience item that would be a great addition to crates.io. – phoenix Feb 10 '19 at 21:36
  • 1
    `Option::unwrap_or` doesn't work for `String` maps using a `&str` default. In this case one should use `Option.map_or("default", String::as_str)`. – dcoles Apr 28 '19 at 12:46
14

What about using entry to get an element from the HashMap, and then modify it.

From the docs:

fn entry(&mut self, key: K) -> Entry<K, V>

Gets the given key's corresponding entry in the map for in-place manipulation.

example

use std::collections::HashMap;

let mut letters = HashMap::new();

for ch in "a short treatise on fungi".chars() {
    let counter = letters.entry(ch).or_insert(0);
    *counter += 1;
}

assert_eq!(letters[&'s'], 2);
assert_eq!(letters[&'t'], 3);
assert_eq!(letters[&'u'], 1);
assert_eq!(letters.get(&'y'), None);
Akavall
  • 82,592
  • 51
  • 207
  • 251
2

.or_insert() and .or_insert_with()

Adding to the existing example for .entry().or_insert(), I wanted to mention that if the default value passed to .or_insert() is dynamically generated, it's better to use .or_insert_with().

Using .or_insert_with() as below, the default value is not generated if the key already exists. It only gets created when necessary.

        for v in 0..s.len() {
            components.entry(unions.get_root(v))
                      .or_insert_with(|| vec![]) // vec only created if needed.
                      .push(v);
        }

In the snipped below, the default vector passed to .or_insert() is generated on every call. If the key exists, a vector is being created and then disposed of, which can be wasteful.

            components.entry(unions.get_root(v))
                      .or_insert(vec![])        // vec always created.
                      .push(v);

So for fixed values that don't have much creation overhead, use .or_insert(), and for values that have appreciable creation overhead, use .or_insert_with().

Todd
  • 4,669
  • 1
  • 22
  • 30
1

A way to start a map with initial values is to construct the map from a vector of tuples. For instance, considering, the code below:

let map = vec![("field1".to_string(), value1), ("field2".to_string(), value2)].into_iter().collect::<HashMap<_, _>>();
orabis
  • 2,749
  • 2
  • 13
  • 29