← Table of contents

2.1 The Absent Task

A negative result is still a result - as everyone knows. It is easy to forget this, especially if you are a tool. A tool that cannot report one tips over the workbench. From the outside, the difference between “not found” and “broken” is obvious. From the inside - not at all.

What You Need

tq can add tasks, iterate through them, and mark them complete. It cannot yet find one specific task by its identifier. The list can be accessed directly: tasks[0], tasks[1]. But that is an index, not an id - and if the requested entry is not there, the list tips over the workbench.

Create a get function: it takes a list and an identifier, and returns the task - or honestly reports that it is not there.

The Build

Rust has a type for this: Option<T> - two variants:

Option<&Task> and &Task are different types. To reach the task inside, both cases must be handled explicitly. With this return type, accidentally ignoring the absence is not possible.

fn get(tasks: &[Task], id: u64) -> Option<&Task> {
    for task in tasks {
        if task.id == id {
            return Some(task);
        }
    }
    None
}

The call goes through match - each variant gets its own path:

match get(&tasks, 1) {
    Some(task) => println!("#{}: {}", task.id, task.title),
    None => println!("not found"),
}

Two tests - two paths:

#[test]
fn get_finds_existing_task() {
    let mut tasks = Vec::new();
    add(&mut tasks, Task::new(1, "Buy coffee"));
    assert!(get(&tasks, 1).is_some());
}

#[test]
fn get_returns_none_for_absent_task() {
    let tasks: Vec<Task> = Vec::new();
    assert!(get(&tasks, 99).is_none());
}

The full test suite is in tq/.

The Result

main below shows only what is new - two calls to get. The full code with list and everything accumulated so far is in tq/.

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

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

    match get(&tasks, 1) {
        Some(task) => println!("#{}: {}", task.id, task.title),
        None => println!("not found"),
    }

    match get(&tasks, 99) {
        Some(task) => println!("#{}: {}", task.id, task.title),
        None => println!("not found"),
    }
}
#1: Buy coffee
not found

make ci passes. get returns either a task or its absence - the calling code decides what to do with either.

The complete tq code for this chapter is in 2-honest/01-the-absent-task/.


Lore: iter().find()

The get function implements a standard search: iterate, check the condition, return the first match. The same can be written with iter().find():

fn get(tasks: &[Task], id: u64) -> Option<&Task> {
    tasks.iter().find(|task| task.id == id)
}

The Option is the same - find returns Some(&Task) if found, None if not. The expanded form is used in the text so that Some and None are visible explicitly. The code in tq/ uses this form - not only for aesthetic reasons: clippy knows the manual_find lint and rejects the expanded loop. make ci will not pass the version from the text unchanged; this is an intentional divergence between the illustration and the working code.


Lore: if let

match requires handling both variants. When only one is needed, if let is shorter:

if let Some(task) = get(&tasks, 1) {
    println!("#{}: {}", task.id, task.title);
}

If the task is not found, nothing happens. For cases where absence requires no separate action, this is more precise than a match with an empty None => {}.