How to provide correct lifetime annotations when dealing with results of manipulations of a `String` created using `String::from`?

Hi all. My current understanding of lifetimes does not understand why I am getting some compiler errors with this piece of code:

enum Inflection {
    Question_NoYelling,
    Question_Yelling,
    Yelling_NoQuestion,
    Other,
}

fn is_whitespace_or_question_mark(c: char) -> bool {
    match c {
        ' ' => true,
        '?' => true,
        '\t' => true,
        '\n' => true,
        _ => false,
    }
}

fn split_message_into_words_and_punctuation<'a>(message: &'a str) -> Vec<&'a str> {
    String::from(message).split(is_whitespace_or_question_mark)
        .filter(|element| element.len() != 0).collect()

}

fn is_uppercase(word: &str) -> bool {
    if word != "?" && word == &word.to_string().to_uppercase() {
        true
    } else {
        false
    }
}

fn get_inflection(message: &str) -> Inflection {
    let mut is_question: bool = false;
    let mut is_yelling: bool = false;

    let words_and_punctuation: Vec<&str> = split_message_into_words_and_punctuation(message);

    for element in  words_and_punctuation.iter() {
        if is_uppercase(element) {
            is_yelling = true;
            break;
        }
    }

    if words_and_punctuation[words_and_punctuation.len() - 1] == "?" {
        is_question = true;
    }

    match (is_question, is_yelling) {
        (true, true) => Inflection::Question_Yelling,
        (true, false) => Inflection::Question_NoYelling,
        (false, true) => Inflection::Yelling_NoQuestion,
        (false, false) => Inflection::Other,
    }
}

fn reply_to_non_empty(message: &str) -> &str {
    match get_inflection(message) {
        Inflection::Question_NoYelling => "Sure.",
        Inflection::Question_Yelling => "Whoa, chill out!",
        Inflection::Yelling_NoQuestion => "Calm down, I know what I'm doing!",
        Inflection::Other => "Whatever.",
    }
}

pub fn reply(message: &str) -> &str {
    let message: &str = String::from(message).trim();
    
    match message {
        "" => "Fine. Be that way!",
        _ => reply_to_non_empty(message),
    }
}

The first error I am getting:

error[E0597]: borrowed value does not live long enough
  --> src\lib.rs:19:5
   |
19 |     String::from(message).split(is_whitespace_or_question_mark)
   |     ^^^^^^^^^^^^^^^^^^^^^ does not live long enough
...
22 | }
   | - temporary value only lives until here
   |
note: borrowed value must be valid for the lifetime 'a as defined on the function body at 18:1...
  --> src\lib.rs:18:1
   |
18 | / fn split_message_into_words_and_punctuation<'a>(message: &'a str) -> Vec<&'a str> {
19 | |     String::from(message).split(is_whitespace_or_question_mark)
20 | |         .filter(|element| element.len() != 0).collect()
21 | |
22 | | }
   | |_^

This is complaining about the function split_message_into_words_and_punctuation, which returns the result of a collect on an iterator. A similar complaint is made about what is returned from calling trim:

pub fn reply(message: &str) -> &str {
    let message: &str = String::from(message).trim();
    
    match message {
        "" => "Fine. Be that way!",
        _ => reply_to_non_empty(message),
    }
}

I have worked through basic examples on lifetime, and I think I get the concept, but I am having trouble understanding why manipulations on the String::from have trouble living long enough when I am returning string literals, which are basically like constants?

1 Like

String::from creates a new owned String value. If this is temporary (i.e. not assigned to a local variable) then it's destroyed at the end of the statement. If it's assigned to a local variable, then it's destroyed when the variable goes out of scope (e.g. function end). In either case, you cannot return a reference (i.e. &str) from a function that references it.

The good news is that your code doesn't need String at all -- all the functionality you need is available on the string slices themselves. Here's your code with adjustments. Note I also simplified your is_whitespace_or_question_mark function to show that you can have multiple patterns in a single match arm.

3 Likes

@vitalyd Thank you! This helps me a lot. I am very surprised though that string slices can support trim (and other String impls). I thought they could only work String because that's where they are implemented. How does that work?

Methods like trim don't change the underlying string, they just change the offset and length of the slice pointing at it:

let a = "  hello world  ";
let b = a.as_slice();
let c = b.trim();

| | |h|e|l|l|o |w|o|r|l|d| | |
\------------- b ------------/
    \--------- c --------/

Since the slice is not responsible for freeing the string, and since the lifetime information is passed through trim(), c is free to point at the substring.

I can see that, but how could I infer that from the documentation, where trim is only mentioned for Strings and not for strs?

On that page, trim is actually mentioned under the heading "Methods from Deref<Target=str>". It's also listed on the doc page for str.

1 Like

Thank you so much! I see now :slight_smile: