Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rust Collect Hashmap from Iterator of Pairs

We have a HashMap, over which we iterate and map to replace the values, but are running into an issue collecting that back to a new HashMap with different value type.

value of type `std::collections::HashMap<std::string::String, std::string::String>`
cannot be built from `std::iter::Iterator<Item=(&std::string::String, std::string::String)>`

What we are doing essentially boils down to this:

let old: HashMap<String, Value> = some_origin();
let new: HashMap<String, String> = old.iter().map(|(key, value)| {
  return (key, some_conversion(value));
}).collect();

The same iterator type is also returned (and not collectable), if one zips two iterators, e.g. in this case zipping key, and the map that only returns the converted value.

new = old.keys().into_iter().zip(old.iter().map(|(key, value)| some_conversion(value)).collect();
like image 543
autarch princeps Avatar asked Jul 30 '20 12:07

autarch princeps


2 Answers

The issue is that iter() (docs) returns a 'non-consuming' iterator which hands out references to the underlying values ([1]). The new HashMap cannot be constructed using references (&String), it needs values (String).

In your example, some_conversion seems to return a new String for the value part, so applying .clone() to the key would do the trick:

let old: HashMap<String, Value> = some_origin();
let new: HashMap<String, String> = old.iter().map(|(key, value)| {
  return (key.clone(), some_conversion(value));
  //         ^---- .clone() call inserted
}).collect();

Here is a link to a full example on the rust playground.

Looking at the error message from the compiler [2], this is indeed quite hard to figure out. I think what most helps with this is to build up an intuition around references and ownership in Rust to understand when references are OK and when an owned value is needed.

While I'd recommend reading the sections on references and ownership in the Rust Book and even more Programming Rust, the gist is as follows:

  1. Usually, values in Rust have exactly one owner (exceptions are explicit shared ownership pointers, such as Rc).
  2. When a value is passed 'by value' it is moved to the new location. This invalidates the original owner of the value.
  3. There can be multiple shared references to a value, but while any shared reference exists, the value is immutable (no mutable references can exist or be created, so it cannot be modified or moved).
  4. We cannot move a value out of a shared reference (this would invalidate the original owner, which is immutable while a shared reference exists).
  5. Usually, Rust doesn't automatically copy (in Rust parlance "clone") values, even if it could. Instead it takes ownership of values. (The exception are "Copy" types which are cheap to copy, such as i32).
  6. (Not relevant here) There can also be a single mutable reference to a value. While this mutable reference exits no shared references can be created.

How does this help?

  • Who owns the keys in the hash map? The hash map does (rule 1)!
  • But how do we get a new key-value pair into the hash map? The values are moved into the hash map (rule 2).
  • But we can't move out of a shared reference ... (rule 3 + rule 4)
  • And Rust doesn't want to clone the value unless we tell it to do so (rule 5)
  • ... so we have to clone it ourselves.

I hope this gives some intuition (again I would really recommend Programming Rust on this). In general, if you do something with a value, you either take ownership over it, or you get a reference. If you take ownership, the original variable that had ownership can no longer be used. If you get a reference, you can't hand ownership to somebody else (without cloning). And Rust doesn't clone for you.

[1]: The docs call this "An iterator visiting all key-value pairs in arbitrary order. The iterator element type is (&'a K, &'a V)." Ignoring the 'a lifetime parameters, you can see that the element type is (&K, &V).

[2]:

13 |         .collect();
   |          ^^^^^^^ value of type `std::collections::HashMap<std::string::String, std::string::String>` cannot be built from `std::iter::Iterator<Item=(&std::string::String, std::string::String)>`
   |
   = help: the trait `std::iter::FromIterator<(&std::string::String, std::string::String)>` is not implemented for `std::collections::HashMap<std::string::String, std::string::String>`
like image 157
Paul Avatar answered Oct 23 '22 11:10

Paul


If you don't need the old map any more you can just use into_iter.

let new: HashMap<String, String> = old.into_iter().map(|(key, value)| {
   return (key, some_conversion(value));
}).collect();

You can see a working version here

like image 2
Michael Anderson Avatar answered Oct 23 '22 11:10

Michael Anderson