1.5 Lending the Work
Passing work around is how a workshop operates. One apprentice polishes, another examines - each does their part, and no one explains the rule because it is understood: whoever receives the piece is the one who controls it. The task carries a name, a shape, and a state. The time has come to pass it along - and to discover what lending differs from giving away. What goes without saying is rarely explained in advance.
The Attempt
Until now, all the logic lived in main: creating a task through its fields and changing
task.status directly. This is hard to read - it needs explicit comments - and harder still
to maintain: with many tasks, every piece of logic would need to be tracked down and updated
individually. The solution is straightforward: extract separate functions - one for creating
a task, one for completing it.
The first - create_task. It takes an id and a title, and returns a new task:
fn create_task(id: u64, title: &str) -> Task {
Task {
id,
title: String::from(title),
status: Status::Todo,
}
}
title: &str is a string passed by reference: the function reads it but does not take it.
String::from(title) creates an owned copy - the same as main did before. This compiles.
In most languages a function returns a value with return. In Rust, return exists too,
but is rarely needed: the last expression in a function body, written without a semicolon,
is the return value. create_task works exactly this way - the Task { ... } constructor
is last and has no semicolon, so the function returns it to the caller.
The second - task_complete. As the previous chapter showed, changing a value requires
mut - and that applies to function arguments too:
fn task_complete(mut task: Task) {
task.status = Status::Done;
}
The parameter mut task: Task - the task is received by value, and mut allows it to be
modified inside the function. The mirror of create_task: the last line ends with ;, so
the function returns nothing. create_task left Task { ... } without ; and returned
the task; task_complete adds ; and returns nothing. The compiler accepts it. Try using
both:
fn main() {
let mut task = create_task(1, "Buy coffee");
println!("{:#?}", task);
task_complete(task);
println!("{:#?}", task);
}
main is now simpler and much more pleasant to read.
But the compiler has a question:
error[E0382]: borrow of moved value: `task`
--> src/main.rs:XX:XX
|
XX | let mut task = create_task(1, "Buy coffee");
| -------- move occurs because `task` has type `Task`, which does not implement the `Copy` trait
XX | println!("{:#?}", task);
XX | task_complete(task);
| ---- value moved here
XX | println!("{:#?}", task);
| ^^^^ value borrowed here after move
What the Compiler Knows
In Rust, every value has one owner. Passing a value into a function means transferring full rights over it - the function becomes the owner, and the previous owner loses access. When the function ends, all its local variables are destroyed along with it.
Except for what it returns - which is exactly why create_task creates a task and hands it
back out: ownership transfers to the caller. The task lives wherever it was received.
task_complete as written takes the task and keeps it. The function ends - the task is
destroyed. The second println! reaches for something that no longer exists.
Modifying a piece without taking it away is a different matter.
The Fix
Instead of passing the whole task, give the function temporary write access - in Rust this
is called borrowing. Instead of Task, use &mut Task:
fn task_complete(task: &mut Task) {
task.status = Status::Done;
}
The function receives a reference, not the value. It can modify the task through the
reference, but does not own it. When the function ends, the reference disappears; the task
in main keeps living.
The call now requires an explicit &mut:
fn main() {
let mut task = create_task(1, "Buy coffee");
println!("Task #{}: {}", task.id, task.title);
println!("{:#?}", task);
task_complete(&mut task);
println!("{:#?}", task);
}
&mut task is a mutable reference to the task. let mut task in main is still required:
without it, a mutable reference cannot be created.
Task #1: Buy coffee
Task {
id: 1,
title: "Buy coffee",
status: Todo,
}
Task {
id: 1,
title: "Buy coffee",
status: Done,
}
make ci passes. The task is created through a function, completed through a function -
and remains accessible to the caller.
The complete
tqcode for this chapter is in1-a-task/05-lending-the-work/.
Lore: Three Ways to Pass a Value
A function receives a value in one of three ways - and each has consequences:
Task- move. The function becomes the owner. After the call, the value is inaccessible to the caller.&Task- shared borrow. The function can read the value; the caller keeps ownership.&mut Task- mutable borrow. The function can modify the value; the caller keeps ownership but must declare the variable withlet mut.
create_task returns a Task - it does not receive ownership, it creates it and hands it
to the caller. That is a fourth outcome: the function as a source, not a recipient.
The same rules apply to any type - not just Task.
Lore: mut on a Binding vs. mut in a Type
The chapter introduced two different muts, and at first glance they look inconsistent:
fn task_complete(mut task: Task) // mut before the name
fn task_complete(task: &mut Task) // mut inside the type
In the first case, mut is a modifier on the binding. It says: “the local variable
task is mutable.” Without it, the line task.status = Status::Done; would not compile -
the compiler catches the attempt to modify something immutable.
In the second case, mut is part of the type. &mut Task is its own type: a mutable
reference to a task. The variable task itself is not mutable - it does not need to be
reassigned, only read and written through. The mutability here describes not the variable
but what it points to.
The rule is simple: mut before a name is about the variable; mut inside a type after
& is about the value on the other side of the reference.