Ownership (part 2)

In this chapter, we'll explore a number of common ownership issues. I'll demonstrate the naive way that is causing the borrow issue and then a proposed solution.

Loops

Issue

fn main() {
    let people = vec!["Marcel", "Tom", "Dick", "Harry"];
    for person in people {
        println!("Hi {person}")
    }
    for person in people {
        println!("Hi {person}")
    }
}

The issue is that while iterating the first time through the people vector, the value held in people is moved to person. This means that at the end of the first loop, people is consumed by the loop, and can no longer be used.

Solution

To fix this case, you can simply borrow from people on the first loop.

fn main() {
    let people = vec!["Marcel", "Tom", "Dick", "Harry"];
    for person in &people {
        println!("Hi {person}")
    }
    for person in people {
        println!("Hi {person}")
    }
}

Optionals

Issue

struct Person {
    first_name: Option<String>,
}

fn main() {
    let person = Person {
        first_name: Some("Marcel".to_string()),
    };

    if let Some(first_name) = person.first_name {
        println!("Hi {}", first_name)
    }

    if let Some(first_name) = person.first_name {
        println!("Hi {}", first_name)
    }
}

The issue here is that person is partially moved into first_name by the first Some(first_name) statement.

Solution

struct Person {
    first_name: Option<String>,
}

fn main() {
    let person = Person {
        first_name: Some("Marcel".to_string()),
    };

    if let Some(first_name) = &person.first_name {
        println!("Hi {first_name}")
    }

    if let Some(first_name) = person.first_name {
        println!("Hi {first_name}")
    }
}

You can solve this quite easily by borrowing person in the initial if let statement. This will actually transform first_name from a String to a &String. Allowing you to use person in the second if let statement.

Async tasks

Problem

use tokio::task::JoinHandle;
use tokio::time::Duration;

struct AnswerToLife {
    answer: i32,
}

impl AnswerToLife {
    fn compute(&mut self) -> JoinHandle<()> {
        let task = tokio::spawn(async move {
            tokio::time::sleep(Duration::from_secs(3)).await;
            self.answer = 42;
        });
        println!("We are computing the answer to life. Please be patient...");
        task
    }

    fn print(&self) {
        println!("The answer to life is {}", self.answer);
    }
}

#[tokio::main]
async fn main() {
    let mut big_question = AnswerToLife { answer: 0 };
    let task = big_question.compute();
    task.await.unwrap();
    big_question.print();
}

If this looks like abracadabra to you, have a look at the chapter on Asynchronous programming.

The problem here is that self is moving into the new tokio::task. This is an issue, because the lifetime of the spawned task can not be determined at compile time. The compiler will hint at introducing a 'static lifetime for self, but that will likely set you off on a wild goose chase. Focus on what you're trying to accomplish. In this case we want to set answer after doing some intense computation. The problem is that in the current data-model we need to have a mutable reference to self to do this. Why don't we solve that, and see if we can change answer, without a reference to self.

Solution

use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::task::JoinHandle;
use tokio::time::Duration;

struct AnswerToLife {
    answer: Arc<Mutex<i32>>,
}

impl AnswerToLife {
    fn compute(&self) -> JoinHandle<()> {
        let lockable_reference_to_answer = self.answer.clone();
        let task = tokio::spawn(async move {
            tokio::time::sleep(Duration::from_secs(3)).await;
            let mut locked_answer = lockable_reference_to_answer.lock().await;
            *locked_answer = 42;
        });
        println!("We are computing the answer to life. Please be patient...");
        task
    }

    async fn print(&self) {
        let locked_answer = self.answer.lock().await;
        println!("The answer to life is {locked_answer}");
    }
}

#[tokio::main]
async fn main() {
    let big_question = AnswerToLife {
        answer: Arc::new(Mutex::from(0)),
    };
    let task = big_question.compute();
    task.await.unwrap();
    big_question.print().await;
}

By wrapping the answer in an Arc we solve two issues at once. compute no longer need a mutuable reference, because the Arc is read-only. And two, we can clone the Arc which is a cheap operation and move the clone into the tokio::task.

Remember that we are cloning the reference to the AnswerToLife structure, not the structure itself.

To allow changes to the answer after computing it, we need to wrap it in a Mutex.

Make sure to use the tokio::sync::Mutex not the one from the standard library.