Basics
It's recommended to follow the example by retyping it in your favorite editor
Imagine we're writing a web crawler that collects posts from different blogs. The crawler's output looks like that:
#![allow(unused)] fn main() { struct DiscoveredItem { blog_url: String, post_url: String, } }
Now we want to write a function operating on the web crawler's results to iterate post URLs from a specific blog.
#![allow(unused)] fn main() { struct DiscoveredItem { blog_url: String, post_url: String, } fn post_urls_from_blog( items: &[DiscoveredItem], blog_url: &str, ) -> impl Iterator<Item = &str> { // Creating an iterator from the &[DiscoveredItem] slice items.iter().filter_map(move |item| { // Filtering items by blog_url if item.blog_url == blog_url { // Returning a post URL Some(item.post_url.as_str()) } else { None } }) } }
Our function doesn't compile, the compiler complains it needs some lifetime annotations.
error[E0106]: missing lifetime specifier
--> src/main.rs:12:27
|
10 | items: &[DiscoveredItem],
| -----------------
11 | blog_url: &str,
| ----
12 | ) -> impl Iterator<Item = &str> {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `items` or `blog_url`
help: consider introducing a named lifetime parameter
|
9 ~ fn post_urls_from_blog<'a>(
10 ~ items: &'a [DiscoveredItem],
11 ~ blog_url: &'a str,
12 ~ ) -> impl Iterator<Item = &'a str> {
|
For more information about this error, try `rustc --explain E0106`.
error: could not compile `playground` due to previous error
If we read the error carefully we can even find a suggestion how to fix our signature.
help: consider introducing a named lifetime parameter
|
9 ~ fn post_urls_from_blog<'a>(
10 ~ items: &'a [DiscoveredItem],
11 ~ blog_url: &'a str,
12 ~ ) -> impl Iterator<Item = &'a str> {
|
Let's use it!
#![allow(unused)] fn main() { struct DiscoveredItem { blog_url: String, post_url: String, } fn post_urls_from_blog<'a>( items: &'a [DiscoveredItem], blog_url: &'a str, ) -> impl Iterator<Item = &'a str> { // Creating an iterator from the &[DiscoveredItem] slice items.iter().filter_map(move |item| { // Filtering items by blog_url if item.blog_url == blog_url { // Returning a post URL Some(item.post_url.as_str()) } else { None } }) } }
Cool, now everything compiles. So that's it, we've won our fight with the borrow checker... right? Well, not quite. The compiler actually tricked us. It provided a suggestion which is semantically incorrect and which made our function over restrictive. That means the borrow checker will strike back soon and will make us scream out of pain. Take a deep breath because in a moment...
Compiler strikes back
Let's try to use our function in some non-trivial context.
struct DiscoveredItem { blog_url: String, post_url: String, } fn post_urls_from_blog<'a>( items: &'a [DiscoveredItem], blog_url: &'a str, ) -> impl Iterator<Item = &'a str> { // Creating an iterator from the &[DiscoveredItem] slice items.iter().filter_map(move |item| { // Filtering items by blog_url if item.blog_url == blog_url { // Returning a post URL Some(item.post_url.as_str()) } else { None } }) } fn main() { // Assume the crawler returned the following results let crawler_results = &[ DiscoveredItem { blog_url: "https://blogs.com/".to_owned().to_owned(), post_url: "https://blogs.com/cooking/fried_eggs".to_owned(), }, DiscoveredItem { blog_url: "https://blogs.com/".to_owned(), post_url: "https://blogs.com/travelling/death_mountain".to_owned(), }, DiscoveredItem { blog_url: "https://successfulsam.xyz/".to_owned(), post_url: "https://successfulsam.xyz/keys_to_success/Just_do_this_one_thing_every_day".to_owned(), }, ]; // Reading the blog URL we're interested in from somewhere let blog_url = get_blog_url(); // Collecting post URLs from this blog using our function let post_urls: Vec<_> = post_urls_from_blog(crawler_results, &blog_url).collect(); // Spawning a thread to do some further blog processing let handle = std::thread::spawn(move || calculate_blog_stats(blog_url)); // Processing posts in parallel for url in post_urls { process_post(url); } handle.join().expect("Everything will be fine"); } // Returns a predefined value fn get_blog_url() -> String { "https://blogs.com/".to_owned() } // Just prints URL out fn process_post(url: &str) { println!("{}", url); } // Actually does nothing fn calculate_blog_stats(_blog_url: String) {}
This code doesn't compile. What's worse is that the compiler error is just absurd:
Compiling playground v0.0.1 (/playground)
error[E0505]: cannot move out of `blog_url` because it is borrowed
--> src/main.rs:45:37
|
42 | let post_urls: Vec<_> = post_urls_from_blog(crawler_results, &blog_url).collect();
| --------- borrow of `blog_url` occurs here
...
45 | let handle = std::thread::spawn(move || calculate_blog_stats(blog_url));
| ^^^^^^^ -------- move occurs due to use in closure
| |
| move out of `blog_url` occurs here
...
48 | for url in post_urls {
| --------- borrow later used here
For more information about this error, try `rustc --explain E0505`.
error: could not compile `playground` due to previous error
How on earth blog_url
that was taken from some kind of a user input is
related to post_urls
returned by the crawler?
The most annoying thing is that the code in the main
function is actually perfectly fine and should compile,
but as you may have already guessed it doesn't because of our malformed function signature.
What's going on here? And what's the error communicating to us?
To answer these questions, we need to understand the way the borrow checker works.