← Table of contents

1.10 The List in Parts

Taking what is needed from the shelf rather than the whole row is not a skill - it is obvious. The obvious has an unpleasant quality: everyone understands it except the tools. The task list is no exception - it can only be viewed in full.

The Attempt

add has a name. The next step is clear: give the review a name too. By analogy with add - &Vec<Task> as the input:

fn list(tasks: &Vec<Task>) {
    for task in tasks {
        println!("#{}: {} - {:?}", task.id, task.title, task.status);
    }
}

A call with one task works:

add(&mut tasks, Task::new(1, "Buy coffee"));
list(&tasks);

But more is needed: showing not the whole list but only part of it. After adding the second and third tasks - only those:

add(&mut tasks, Task::new(2, "Buy milk"));
add(&mut tasks, Task::new(3, "Buy eggs"));
list(&tasks[1..3]);

The compiler responds:

error[E0308]: mismatched types
  --> src/main.rs:XX:XX
   |
XX |     list(&tasks[1..3]);
   |     ---- ^^^^^^^^^^^^ expected `&Vec<Task>`, found `&[Task]`
   |     |
   |     arguments to this function are incorrect

What the Compiler Knows

&tasks[1..3] is not a reference to a Vec. It is &[Task]: a reference to a contiguous section of tasks, starting at index 1 and up to 3 (not including). A slice does not know where the data comes from - a Vec, an array, another slice.

&Vec<Task> and &[Task] are different types. A function with a &Vec<Task> parameter accepts only a reference to a whole Vec - &[Task] does not fit. The reverse works differently: if a function takes &[Task], it can receive both &tasks (the full list) and &tasks[1..3] (a part). Rust automatically coerces &Vec<Task> to &[Task] at the call site - this is a built-in rule of the language.

Range syntax:

ExpressionWhat it takes
&tasks[1..3]index 1 up to 3 (not including)
&tasks[1..]index 1 to the end
&tasks[..2]from the start up to 2 (not including)
&tasks[..]the whole list

The Enchantment

One change - &Vec<Task> to &[Task]:

fn list(tasks: &[Task]) {
    for task in tasks {
        println!("#{}: {} - {:?}", task.id, task.title, task.status);
    }
}

Now list accepts both the whole list and any part of it. Two calls in main:

fn main() {
    let mut tasks: Vec<Task> = Vec::new();

    add(&mut tasks, Task::new(1, "Buy coffee"));
    list(&tasks);

    add(&mut tasks, Task::new(2, "Buy milk"));
    add(&mut tasks, Task::new(3, "Buy eggs"));

    let task = &mut tasks[0];
    task.complete();

    if task.is_done() {
        println!("Task 1 done");
    }

    list(&tasks[1..]);
}
#1: Buy coffee - Todo
Task 1 done
#2: Buy milk - Todo
#3: Buy eggs - Todo

make ci passes. list takes a slice - and can now show any part of the list.

The complete tq code for this chapter is in 1-a-task/10-the-list-in-parts/.


Lore: A Slice and the Whole List

list(&tasks) also works - passing the full list without specifying a range. &tasks has type &Vec<Task>, but when passed to a function with a &[Task] parameter, Rust coerces it automatically.

In practice this means: functions that only read a list are better written with &[Task]. They become more general - they work with any source of data, not just a Vec.