almendar
almendar

Reputation: 1813

Lifetime of variable in map/flat_map in Rust

I think I have a good understanding of lifetimes, but what I've read doesn't tick with what the compiler says when it comes to closures ;)

I have a function with the signature:

fn split_to_words(content: &str) -> Vec<String>

With for loops it looks like this:

let mut acc: Vec<String> = Vec::new();
for line in content.lines() {
    if line.is_empty() { continue };
    for word in line.split_whitespace() {
        acc.push(word.to_lowercase());
    }
}

and using iterators:

let acc: Vec<String> = content.lines()
      .filter(|x| !x.is_empty())
      .map(|x| x.to_lowercase())
      .flat_map(|x: String| x.split_whitespace())
      .map(|x| x.to_string())
      .collect();

but I end up with an error:

error: `x` does not live long enough

.flat_map(|x: String| x.split_whitespace())
                      ^

Upvotes: 1

Views: 1760

Answers (2)

Shepmaster
Shepmaster

Reputation: 430514

It's helpful to review the entire error message:

error: `x` does not live long enough
note: reference must be valid for the method call at ...
note: ...but borrowed value is only valid for the scope
      of parameters for function at ...

When flat_map is called, you pass ownership of the String to the closure. However, the closure tries to return an iterator that contains references to the String. Since each String will be deallocated after the closure call ends, the reference into it would be invalid, so the compiler prevents you from doing this.

I'd probably use the for-loop variant or a similar version:

fn split_to_words(content: &str) -> Vec<String> {
    let lines = content.lines()
           .filter(|x| !x.is_empty())
           .map(|x| x.to_lowercase());

    let mut result = Vec::new();

    for line in lines {
        for word in line.split_whitespace() {
            result.push(word.to_string());
        }
    }

    result
}

fn main() -> () {
    println!("{:?}", split_to_words("hello world"));
}

In this particular case though, I think you could write it as:

fn split_to_words(content: &str) -> Vec<String> {
    content.split_whitespace().map(str::to_lowercase).collect()
}

Newlines are whitespace, so there's no need to use lines. This also removes the possibility of blank values.

Upvotes: 2

Francis Gagn&#233;
Francis Gagn&#233;

Reputation: 65692

Your iterator-style code isn't exactly the same as your imperative-style code: in the imperative code, you do split_whitespace before to_lowercase, whereas in the iterator code, you do the opposite. It turns out that doing split_whitespace before is more efficient, since split_whitespace doesn't need to allocate Strings; it only returns slices into the given string slice. On the other hand, to_lowercase needs to allocate a new string, so by doing it last, we can save an allocation.

let acc: Vec<String> = content.lines()
      .filter(|x| !x.is_empty())
      .flat_map(|x| x.split_whitespace())
      .map(|x| x.to_lowercase())
      .collect();

Upvotes: 3

Related Questions