3.5 A Voice of Its Own
Signing an obligation is not the last step. A craftsman who can make pieces but cannot describe them works for one audience. A technical report is precise and complete - for the person who compiled it. This is usually called precision. The accurate word is half the work.
The Attempt
Right now a task is printed by hand, field by field:
// src/main.rs
for task in store.all() {
println!("#{}: {} - {:?}", task.id, task.title, task.status);
}
The data is correct. But the format is a decision made by main, not by the task. Try
demanding that the task speak for itself. Replace the output line with:
// src/main.rs
println!("{}", task);
The compiler notices:
error[E0277]: `Task` doesn't implement `std::fmt::Display`
--> src/main.rs:XX:XX
|
XX | println!("{}", task);
| ^^^^ `Task` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Task`
= note: in format strings you may be able to use `{:?}` (or `{:#?}` for pretty-print) instead
{:?} works because #[derive(Debug)] is already on Task. {} requires something
different.
What the Compiler Knows
{} and {:?} are two different calls to two different traits.
{:?} calls fmt::Debug. It is generated by #[derive(Debug)]: it shows fields and
values as they are stored in memory. That is exactly why it can be derived automatically -
the structure is already known to the compiler.
{} calls fmt::Display. It cannot be derived automatically: the compiler does not know
what “human-readable” means for your type. That is the author’s decision - and only theirs.
The trait is std::fmt::Display. To avoid writing the full path every time, add this at
the top of src/main.rs:
// src/main.rs
use std::fmt;
use and modules are covered in chapter 3.6; for now it is enough to know that
fmt::Display and std::fmt::Display are the same thing.
The trait requires one method:
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
fmt::Formatter<'_> is the receiver for the formatted string; write!(f, ...) writes
into it. The apostrophe '_ is shorthand for a reference lifetime - that is covered in
chapter 6. Copy the signature as-is.
write!(f, ...) works like println!, but writes into f. The last write! in the
method is returned directly - it is the fmt::Result.
The Enchantment
In src/main.rs, after impl From<&str> for Task:
// src/main.rs - NEW
impl fmt::Display for Task {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "#{}: {} [{:?}]", self.id, self.title, self.status)
}
}
{:?} for status - Status is a simple enum with no data; its Debug output (Todo,
Done) reads as well as any hand-written alternative.
The same trait for errors. In src/main.rs, after the TqError declaration:
// src/main.rs - NEW
impl fmt::Display for TqError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TqError::EmptyTitle => write!(f, "task title cannot be empty"),
TqError::NotFound(id) => write!(f, "task {} not found", id),
}
}
}
In main, remove the hand-assembled string. The loop becomes:
// src/main.rs - CHANGED: manual string → Display
for task in store.all() {
println!("{}", task);
}
Every place where errors were printed with {:?} switches to {}. For example:
// src/main.rs - CHANGED: {:?} → {}
Err(e) => println!("rejected: {}", e),
There is one further consequence. Previously main matched NotFound in a separate arm
and printed the message by hand:
Err(TqError::NotFound(id)) => println!("task {} not found", id),
Now that duplicates Display. The arm in that form is redundant:
// src/main.rs - CHANGED: specific arm removed, Display is enough
match store.get(99) {
Ok(task) => println!("{}", task),
Err(e) => println!("{}", e),
}
Output:
#1: Buy coffee [Todo]
#2: Buy milk [Todo]
#3: Buy eggs [Todo]
found: #1: Buy coffee [Todo]
task 99 not found
done: true
rejected: task title cannot be empty
Two tests fix both formats:
#[test]
fn task_display_todo() {
let task = Task::new(1, "Buy coffee").unwrap();
assert_eq!(format!("{}", task), "#1: Buy coffee [Todo]");
}
#[test]
fn error_display_not_found() {
assert_eq!(format!("{}", TqError::NotFound(99)), "task 99 not found");
}
make ci passes. The task can describe itself - main no longer assembles strings from
fields.
The complete
tqcode for this chapter is in3-a-voice/05-a-voice-of-its-own/.
Lore: std::error::Error
Display is the first half of the standard contract for error types. The standard library
has a trait std::error::Error, and it requires Display as a precondition: an error must
be able to describe itself in plain language.
TqError does not yet sign this contract. But when file I/O errors arrive, libraries will
require std::error::Error for compatibility. Display is already in place - that is one
of the two lines needed.