← Table of contents

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 tq code for this chapter is in 1-a-task/05-lending-the-work/.

Scroll: The simplest way to understand ownership in Rust


Lore: Three Ways to Pass a Value

A function receives a value in one of three ways - and each has consequences:

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.