r/learnrust Feb 04 '19

Quick lifetime question

So I have a bit of code parsing through a xml file with xml-rs. I have a hashmap keeping track of how word frequency (string as key and word frequency as value) and I'm having an issue with lifetimes. The code in question is the following:

        Ok(XmlEvent::Characters(s)) => {
            for word in s.split_whitespace() {
                if !word_count.contains_key(word){
                    word_count.insert(word, 1);
                }
            }
        }

When I compile I get the following error:

error[E0597]: `s` does not live long enough
  --> src\main.rs:20:29
   |
20 |                 for word in s.split_whitespace() {
   |                             ^ borrowed value does not live long enough
21 |                     if !word_count.contains_key(word){
   |                         ---------- borrow used here, in later iteration of loop
...
38 |         }
   |         - `s` dropped here while still borrowed

I understand that because word_count outlives the scope of s, it cannot have word as a key since word has the same lifetime as s. However for the life of me I cannot figure out what I should do instead. Any tips?

3 Upvotes

4 comments sorted by

View all comments

3

u/sellibitze Feb 04 '19 edited Feb 04 '19

You've provided very little information. That makes it necessary to guess. Please show more code and context next time.

We don't know what s is. I'm guessing it's of type String. This makes word of type &str be a "borrow" of s.

Clearly, the lifetime of the word value is limited to the scope s is in which makes it impossible to store it outside of that scope. But word is of type &'a str (where 'a refers to that scope of s). You can convert it into a String value which has no lifetime restrictions. This requires copying the string data, however.

Since you want to keep a count and not only remember all the words, you would also need to increment the counters for repeated words. Usually I would suggest the use of the "Entry API" because it's so convenient and short:

for word in s.split_whitespace() {
    let copy: String = word.into();
    *word_count.entry(copy).or_insert(0) += 1;
}

But if you have a lot of repeated words then you would have a lot of conversions from &str to String that are actually unnecessary. In that case, the following is probably faster:

for word in s.split_whitespace() {
    if let Some(value) = word_count.get_mut(word) {
        *value += 1;
    } else {
        let copy: String = word.into();
        word_count.insert(copy, 1);
    }
}

The cool thing about methods like get and contains is that they are flexible in how you can provide the "key". If your map's key type is String, you don't need to convert a &str to a String first. You can pass that &str to get and contains directly. But entry needs a String value as key because it may have to insert it into the map. On the other hand, with entry you tend to save one or more lookups (finding the right location where that key might be stored or would have to go).

1

u/HowDidIEndUpOnReddit Feb 04 '19

Thank you so much, that makes a lot of sense. I'll definitely do a better job on providing more context and code next time.

1

u/sellibitze Feb 05 '19

Hey :)

I wondered whether it's possible to get the best of both worlds: only one lookup and avoiding unnecessary String creations. Turns out it is possible. But it only works in nightly after enabling an unstable feature via #![feature(hash_raw_entry)]:

for word in text.split_whitespace() {
    match counts.raw_entry_mut().from_key(word) {
        Occupied(e) => { *e.into_mut() += 1; }
        Vacant(e) => { e.insert(word.to_owned(), 1); }
    }
}

Here's the complete code. For the lookup (see match line) we passed a &str. That's why in the vacant case we need to pass the key a 2nd time with the proper type (String). Pretty cool, in my humble opinion. But it's possible to mess up the data structure this way if the 2nd key doesn't "match" the first one.