This note takes one small, very common piece of Rust and explains every word in it: a struct, plus an impl fmt::Display block that controls how the struct prints with println!("{}", ...).
It uses &self and borrowing from Note 01, so read that first if & still looks mysterious.
The code for this note
From src/math/matrix.rs in the demo project:
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DimensionMismatch {
pub left: usize,
pub right: usize,
}
impl fmt::Display for DimensionMismatch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"dimension mismatch: the left vector has {} components, the right vector has {}",
self.left, self.right
)
}
}
DimensionMismatch is an error value: “you tried to combine a vector with left components and one with right components”. Ignore the #[derive(...)] line for now; Note 06 is all about it. Here is each remaining part, in order.
1. struct: a data type you define
A struct is a data type that you define. It groups named fields:
pub struct DimensionMismatch {
pub left: usize,
pub right: usize,
}
usize is an unsigned (never negative) integer, the type Rust uses for sizes and indexes. pub makes the struct and each field visible outside this module (see Note 10).
The Python equivalent is a class that only has fields:
class DimensionMismatch:
def __init__(self, left, right):
self.left = left
self.right = right
There’s one important difference. In Python, a class holds its data and its methods together. In Rust, the struct holds only data. Behaviour goes in a separate block, called impl.
2. impl: “implement”, adding behaviour to a type
impl means “implement”. It attaches behaviour to a type, and it comes in two forms:
// Form A: your own methods (an "inherent impl")
impl DimensionMismatch {
pub fn total(&self) -> usize {
self.left + self.right
}
}
// Form B: implement a trait
impl fmt::Display for DimensionMismatch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { /* ... */ }
}
- Form A adds methods that only this type has. You call it as
e.total(), which returns10forleft: 4, right: 6. - Form B fulfils a contract that is defined somewhere else. Read it aloud as: “Implement the trait
Displayfor the typeDimensionMismatch.”
The code in this note uses Form B.
3. fmt::Display: the trait that decides what {} prints
Display is a trait from the standard library. A trait is a contract: “any type that has this trait must provide these methods”. It’s similar to an interface in Java or a protocol in Python. (Note 05 covers traits in general: defining your own, generics and dyn Trait.)
The standard library defines Display like this (simplified):
pub trait Display {
fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}
So the contract has exactly one method, fmt. Once your type provides it, println!("{}", value) knows how to print your type.
Display is the text meant for a human user. Its sibling Debug is meant for the programmer:
| Trait | Placeholder | Who reads it | Python | How you get it |
|---|---|---|---|---|
Debug |
{:?} |
the programmer | __repr__ |
#[derive(Debug)] |
Display |
{} |
the user | __str__ |
write it by hand |
For the same value:
{} -> dimension mismatch: the left vector has 4 components, the right vector has 6
{:?} -> DimensionMismatch { left: 4, right: 6 }
Rust can’t derive Display, because the compiler can’t guess which message you want a user to see. So you write it yourself.
4. Why fmt appears twice with different meanings
The name fmt shows up twice in impl fmt::Display ... fn fmt(...), but the two mean different things:
fmt::Display: herefmtis the modulestd::fmt, brought in byuse std::fmt;(see Note 10).fn fmt(...): herefmtis the method that theDisplaycontract requires.
You must use the name fmt, because the contract says so. Name it anything else and the compiler refuses:
impl fmt::Display for D {
fn format(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "d") }
}
error[E0407]: method `format` is not a member of trait `fmt::Display`
error[E0046]: not all trait items implemented, missing: `fmt`
It’s the same in Python: the method must be called __str__, and Python won’t call a method with any other name.
5. The parameters, one by one
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
&self: the value being printed
self is the value itself, like self in Python. The & means borrow: the method reads the value but doesn’t take ownership. So after println! runs, you can still use the value.
| Form | Meaning |
|---|---|
self |
take ownership (the value moves into the method) |
&self |
borrow to read |
&mut self |
borrow to change |
Printing only reads the data, so &self is the right choice. Note 01 explains these three forms in depth.
f: &mut fmt::Formatter<'_>: where the text goes
f is the destination for the text. It might be the terminal, a String or a file. Your code doesn’t need to know which, because it just writes into f.
&mutis a mutable borrow. Writing text intofchanges it, so the method needs permission to change it.<'_>is a lifetime: a label that tells the compiler how long a borrowed reference stays valid.Formatterholds a reference inside it, and'_means “compiler, work out the correct lifetime yourself”. You don’t need to name it here. (Writing plainfmt::Formatteralso compiles; the<'_>just makes the hidden reference visible.)
-> fmt::Result: success or failure
-> introduces the return type. fmt::Result is short for Result<(), fmt::Error>, which has two possible values:
Ok(()): the text was written.(), called “unit”, means “no value”, a bit likeNonein Python.Err(fmt::Error): the destination couldn’t accept the text.
Writing can fail, so Rust makes the failure part of the return type, and the caller has to deal with it. Python would raise an exception instead. In practice, a Display method just passes on whatever write! returns. It never invents an error of its own.
6. write!: a macro, like println!
The ! tells you that write! is a macro: code that writes code while the program compiles.
Why a macro and not a function? A Rust function has a fixed number of arguments, but a macro can take any number. The macro also checks the format string while the code compiles. Use two {} but give only one value, and the build fails:
let a = 1;
println!("{} and {}", a);
error: 2 positional arguments in format string, but there is 1 argument
Python would only find this mistake at runtime.
write!, println! and format! work almost the same way. Only the destination differs:
| Macro | Destination | Returns |
|---|---|---|
println!("...", a) |
the terminal (standard output), plus a newline | nothing |
write!(f, "...", a) |
the destination f |
fmt::Result |
format!("...", a) |
a brand-new String |
the String |
Each {} is replaced by the next value, in order:
"...the left vector has {} components, the right vector has {}"
↑ ↑
self.left self.right
No semicolon on the last line
Look closely: the write!(...) call has no ; after it. In Rust, the last expression of a block, written without a ;, is the block’s return value. So the method returns whatever fmt::Result write! produced.
Add a ; and the expression becomes a statement, the method returns (), and the compiler stops:
error[E0308]: mismatched types
expected `Result<(), Error>`, found `()`
help: remove this semicolon to return this value
7. How the parts connect
let e = DimensionMismatch { left: 4, right: 6 };
println!("{}", e);
println!sees{}, so it needsDisplayfore.- It creates a
Formatterthat writes to the terminal. - It calls your method,
e.fmt(&mut formatter), lendingewith&. - Your
write!puts the text into the formatter. - The output is:
dimension mismatch: the left vector has 4 components, the right vector has 6
flowchart LR
A["println!(#quot;{}#quot;, e)"] --> B["needs Display for e"]
B --> C["creates a Formatter<br/>(to the terminal)"]
C --> D["calls e.fmt(&mut formatter)"]
D --> E["write! puts text<br/>into the formatter"]
E --> F["Ok(()) returned"]
Because you implemented Display, you also get .to_string() and format! support for free:
let text: String = e.to_string();
let line = format!("error: {e}");
Summary: Rust structs, impl and Display
structis a data type you define. It holds only data.impl Typeadds your own methods.impl Trait for Typefulfils a trait’s contract.Displaycontrols the output of{}, like__str__in Python, and must be written by hand.Debug({:?}) can be derived.- The method must be called
fmt, because the trait requires that name. &selfborrows the value.&mut Formatter<'_>is the destination you write into.fmt::Resultreports success (Ok(())) or failure (Err).write!is a macro likeprintln!but writes tof, and it checks the format string while the code compiles.- The last line has no
;, so its value is the return value.