Rust: Returning reference to HashMap value from a function | SoloLearn: Learn to code for FREE!

+18

Rust: Returning reference to HashMap value from a function

My intention was to implement a cacher structure which can cache multiple arguments and return values using a HashMap. Of course I would run into issues with regards to reference lifetimes and borrows, to be precise: in the use of both .get() [immutable borrow] and .insert() [mutable borrow] over the required lifetime of the return value. I have now avoided that problem by requiring the value type to implement the Copy trait and return a copy instead of a reference. The example code is: https://code.sololearn.com/chV1H160UTrO (Of course, this code doesn't run on SoloLearn) It works, but I would like to know if (and how) it would be possible to return a reference instead, what would be the cost in returning a copy, and what would be an rust-idiomatic way of implementing this? Thanks!

12/1/2020 12:16:52 PM

Coder Kitten

27 Answers

New Answer

+12

Sorry for being a little offtopic, I am not a Rust programmer, but I tremendously enjoy reading this thread. I wish most questions on SL were of similar quality... It would definitely motivate me to spend more time in the forum 😅 A fresh one on Rust's growing popularity: https://www.nature.com/articles/d41586-020-03382-2

+11

XXX I'm not sure if you stumbled onto this post on your own or if you're answering the call from my earlier post. Regardless, I'm glad you did as this has quickly become one of my favorite Q&A discussions in a long while. It's gotten me digging into the Rust documentation to fill in the gaps of this thread. It's been a long while since I've had to research something new like this. Coder Kitten I want to give you kudos for asking such a great question that has revealed some intricate aspects of this language that has further piqued my interest beyond my surface level awareness. It's the first time since delving into a language like Elixir that I've felt this intrigued about a language. This post might just be the point that nudged me over the edge to carve out time and learn this language. Thanks guys for driving such a great thread. 🙏

+8

Oh... and by the way, Amazon just started a campaign to aggressively hirer Rust developers and are doubling down in their commitment to this language. I also know that Microsoft has taken a strong interest in Rust. This could be a solid language for people looking to accelerate their careers. https://www.zdnet.com/article/amazon-were-hiring-software-engineers-who-know-programming-language-rust/

+8

Tibor Santa I completely agree with you. If you look at the Discuss tab in this app, this question is ranked 6th in Hot Today list. It's truly sad to see the other five Q&A Posts currently ranked higher than this one. Let's cherish this brief moment of a quality thread and hope it somehow leads to more like this one. *Edit:* This moved to 4th position. But the point is still the same. It's a lone sample of what could be.

+7

Coder Kitten [Part 1] Quoted from the book (https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html#only-inserting-a-value-if-the-key-has-no-value) "The or_insert method on Entry is defined to return a mutable reference to the value for the corresponding Entry key if that key exists, and if not, inserts the parameter as the new value for this key and returns a mutable reference to the new value. This technique is much cleaner than writing the logic ourselves and, in addition, plays more nicely with the borrow checker." std::collections::HashMap::entry - https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.entry std::collections::hash_map::Entry - https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html std::collections::hash_map::Entry::or_insert - https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html#method.or_insert

+7

Coder Kitten [Part 2] For this to work however, you will need to change lines 6 and 16 from T: Fn(U) -> R to T: Fn(&U) -> R This is because we don't want the function to take the ownership of the key. I also believe that this will prevent problems in the long run, without actually affecting the code much. Taking references is much better than taking by value. Here is a working example https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=5c3dd639b62bdc48b00ce2d21b3e4aaa

+7

Coder Kitten [Part 2] The best solution IMO, would be using std::rc::Rc. You probably know about it if you have read the official book. The only downside is that it have a little bit of runtime cost because it allocates on the heap, but that would be very minute, as compared to using `Copy` or `Clone` (imagine cloning a 100 element vector of 100 element vectors). std::rc::Rc - https://doc.rust-lang.org/std/rc/struct.Rc.html Rc in the rust official book - https://doc.rust-lang.org/book/ch15-04-rc.html Working example https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=95b8c72a62a20249400d234aa1df0672 Although I still believe using Rc here would be unnecessary. Maybe Aaron Eberhardt can help. He knows Rust.

+7

Coder Kitten I ran your code on sololearn. https://code.sololearn.com/caEKq4O1489z/?ref=app

+6

XXX Thanks a lot! Yes, that looks much better 😊 I had a feeling there would be functions that would just work better here. Well, that is why I asked. Thanks again.

+6

XXX I found a minor flaw in your example - it does cache, but the idea is that the cached result is returned without invoking the closure. However, .contains_key() appears to do the trick, or to put it another way, I cannot use if let this way without violating borrow rules. I rewrote it like this now: pub fn value(&mut self, arg: U) -> &R { if self.results.contains_key(&arg) { println!("cached"); return self.results.get(&arg).unwrap(); } let r = (self.f)(&arg); self.results.insert(arg, r); self.results.get(&arg).unwrap() } Thanks again, you certainly highlighted good points there and gave me some new ideas.

+6

Coder Kitten [Part 1] Oh yes, my solution forgets the whole point of cache. But... are you sure the way you rewrote it works? It should give an error as on the line `self.results.insert(arg, r)` you are moving `arg` into the the scope of the `insert` method, which is why it is unusable on the line `self.results.get(&arg).unwrap()` I even checked https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=6ae88c74b0e61b4449c2398222072c1a It might be working for you because you probably forgot to remove the constraint of the `Copy` trait on type parameters `R` and `U`, i.e. you might have forgotten to remove `+ Copy` on line 7 and 17, and `R: Copy` on line 8 and 18 (in the code mentioned in the question body).

+6

Here's my version that only requires Clone and no Copy: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=5a93e0f0d204e3df5f7ea42277d8b931

+6

@Kiwwi, I personally will refrain from making any course requests. I can imagine they have enough on their minds and probably not the personnel educated well enough in Rust to design a course, assignments and challenges. Besides that, I believe that, if a language is not available on SoloLearn then it is my own responsibility to find other resources. The official book is a good start, the documentation is very elaborate, plus they have a playground. From following the Q&A I knew that there are Rustaceans using SoloLearn, so I knew wouldn't be alone with my questions and that there might be others possibly interested in this discussion :).

+6

[Off-topic] David Carroll Tibor Santa I think that this one of the features of Rust I really like. It compels you to think about a problem from a perspective you probably wouldn't in other languages, which results in you coming out with an even better, optimized solution. In this case, garbage collected languages would simply add to the reference count of the variable and simple increase the validity scope of the variable, and others will maybe just pass out a copy of the value. But Rust is right now making us think how do we use values in different scopes without copying them or making their references dangling, and at the same time not suffer the runtime cost of a garbage collector.

+5

Anyone familiar enough with Rust to answer this question? Coder Kitten The linked code snippet is so pleasant to look at. This is the appeal that has so many experienced developers so intrigued about the Rust language. I would try to answer, but it wouldn't be based on knowledge gained from first hand experience with Rust. For this reason, I've posted this request to my network hoping it casts a wider reach on your behalf. 😉

+5

XXX Yes, you are right. I was wondering about those two lines for quite some time, how it was possible to move the value into the HashMap but still be able to use it right thereafter. Well, we have so far managed to get the Copy trait off the return type R, but the key type U still requires it, which makes the move and reuse possible, as you said. I need to revisit the section about the smart pointers and see if and how it will help - once I have a little more time on my hands. But Aaron Eberhardt just chimed in, maybe he has some more good ideas.

+5

Coder Kitten that's a very interesting question and to be honest I've never encountered such a hard ownership problem. First, the thing with the Copy trait is, that it implies that copying the data type is very efficient. That's great for small structs and primitive types but not for everything. The Clone trait however is pretty common and does not imply efficiency on cloning a data type. So you probably want to reduce the requirements from Copy to Clone to make it more general purpose. But of course that does not solve any ownership problems, it just makes it visible where the cloning (or copying) of an data type happens. The ownership problem boils down to this: The HashMap owns the keys and the values yet you need to return the value without a reference (because you can't return references to local variables). This makes it impossible to borrow which means you need to clone the data... Smart pointers won't help either but maybe RefCell or Cow could work, by enforcing borrow rules during runtime.

+5

Thanks, Aaron Eberhardt. It will sure be interesting reading the source to the cached crate ☺. I am so far quite happy actually that we got the copy trait off the return type and .value() does return a reference now. But still, any new ideas and suggestions are always welcome.

+5

Coder Kitten I must admit I was partially wrong xD I just couldn't accept that Rust was unable to actually do this without copying the data every time which would be super inefficient so I though about it a little more. So here's what I came up with: The HashMap owns all the data, which makes sense. But this also means that you can't really borrow the data in this context because you could delete or replace the data later which would invalidate borrowed references which would be unsafe. This means, you can't eliminate the need to call clone somewhere because Rust won't allow you to move any data out of the HashMap. But you can at least use Rc, like XXX suggested to only clone a pointer, not the data. Rc guarantees Rust that there will always be a valid reference which solves the problem and even eliminates the Clone dependency for the R type. I think this could be the solution for your problem: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=4f5da3122172a59f74a8b3edd7b0f668

+5

(a bit off-topic) the reason don't have dive into Rust is that isn't here in SL yet. so why not write all an email with same title and send it to SL asking for include Rust. Just think about; lessons, assignments, challenges.. If someone with many followers, David Carroll for example, posts it, maybe enough people joins and SL team hears us.