46

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
5
  • 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? Commented Jan 1, 2017 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 } Commented Jan 1, 2017 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 editing your question to show example (pseudo)code of what you'd be able to do if such a default exists. Commented Jan 1, 2017 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. 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. Commented Jan 1, 2017 at 19:04
  • 1
    @ArtemGr that's also interesting in Rust as it means that a get would need to mutate the hashmap. Commented Jan 1, 2017 at 19:09

5 Answers 5

66

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.

Sign up to request clarification or add additional context in comments.

4 Comments

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.
@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, 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 stackoverflow.com/a/36154859/155423.
This is the kind of little convenience item that would be a great addition to crates.io.
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).
15

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

1 Comment

Yes, that would be why I linked to stackoverflow.com/q/28512394/155423 in my answer.
4

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

Comments

2

Similar to other answers based on Entry, but there is also or_default if you want to use the default value:

*map.entry(key).or_default() += 1

The default works for Vec as well:

map.entry(key).or_default().push(value)

Comments

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<_, _>>();

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.