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


#1

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?


#2

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

@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?


#4

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.


#5

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


#6

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.


#7

Thank you so much! I see now :slight_smile: