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:
Some(value)- the value is presentNone- the value is absent
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 form above is expanded so that Some and None are visible where they appear.
The working version is shorter:
fn get(tasks: &[Task], id: u64) -> Option<&Task> {
tasks.iter().find(|task| task.id == id)
}
To the compiler both forms are identical: find can be read as the loop above - the same
iteration, the same condition, the same result.
Building the habit of reaching for iterators instead of for starts now. In this specific
case the difference is only in size - the real power shows in chains: .filter(), .map(),
.find() one after another without nested loops. When we get there - the hand will already
know where to reach. |task| task.id == id is a closure, a topic of its own.
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
tqcode for this chapter is in2-honest/01-the-absent-task/.
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 => {}.