Two lessons ago, this course made a promise about a function called read_count_from_file: implement one more trait, and watch two .map_err(...) calls "melt away into nothing, replaced by two bare question marks." One lesson ago, it made a second promise — that PartialOrd, Display, and Debug were "about to click into one coherent picture, all at once." Both promises are due today. Same idea, paying off twice.
What a trait actually is
Start from scratch, with the simplest version of the idea. A trait names a piece of behavior — one or more method signatures — without saying anything about which types have it, or how. Then, separately, any type at all can declare "I have that behavior, and here's exactly how it works for me":
Read the trait itself first — trait Summary { fn summarize(&self) -> String; }. That semicolon, where a method body would normally go, is the entire idea right there: this is a promise, not an implementation. It says "anything that calls itself a Summary can have .summarize() called on it, and it'll hand back a String" — and says nothing whatsoever about how. Now look at the two impl Summary for ... blocks. NewsArticle and Tweet share no fields, no parent type, nothing structural in common at all — and yet both can now be handed to any code that only cares about one thing: can I call .summarize() on this? A trait is a promise about what a type can do, completely independent of what a type is.
And here — already — is the first promise, paid off. PartialOrd, from last lesson's <T: PartialOrd>, is a trait, defined somewhere in the standard library in essentially this same shape — a method signature along the lines of "compare yourself to another value of your own type," with no body — and then an impl PartialOrd for ... block for every built-in type where comparison makes sense: every number, char, and plenty more. T: PartialOrd was never a special, mysterious kind of constraint. It said exactly what &impl Summary is about to say in the next section: "T can be any type at all — so long as somewhere, an impl PartialOrd for T block exists."
Default implementations
A trait method doesn't have to end in a semicolon. It can come with a body of its own — a default, used automatically by any type that doesn't bother writing one of its own. Suppose Summary had been defined slightly differently:
Tweet writes its own summarize, exactly as before, and that version wins — its own words take priority over the default. NewsArticle, this time, writes impl Summary for NewsArticle {} — and stop there for a second, because that empty pair of braces is doing real work, not nothing. It isn't a stub, and it isn't unfinished. It's a complete, valid sentence: "I have nothing to add — whatever the default says is exactly right for me." NewsArticle's .summarize() now quietly returns "(Read more...)", having had zero lines of logic written to make that happen. Hold onto that shape — an entirely empty impl block, meaning something real — because it's about to reappear, soon, doing genuinely important work.
Traits as parameters — a shape you've already seen
One more piece of syntax, and then this lesson turns toward the payoff. Here are two ways to write "this function accepts anything that can summarize itself" — and they aren't merely similar. They're the same:
&impl Summary reads almost like English: "a reference to — anything — so long as it implements Summary." And <T: Summary>? Recognize it. That is, character for character, the same shape as <T: PartialOrd> from last lesson, with one trait's name swapped for another's. They aren't two related ideas — they're the exact same mechanism, the exact same compile-time monomorphization from last lesson, two different ways of writing it down. &impl Trait is shorthand for the single-parameter case; reach for the full <T: Trait> form the moment a type needs to show up more than once — two parameters that must match each other, say, or a return type tied to an input.
What #[derive(...)] was doing this entire time
Here's the second promise, paid off — and it needs no new code at all, because you just wrote the shape it's been hiding behind. Every single #[derive(Debug)] you've ever written tells the compiler: write an impl Debug for YourType block, with a body that walks through every field on YourType and prints its name and value — in exactly the shape you just used for impl Summary for NewsArticle above, except the compiler writes the body for you, automatically, every single time. derive was never a separate language feature, off doing its own unrelated thing in a corner. It's the compiler quietly writing impl Trait for Type blocks on your behalf, for a small, specific list of traits common enough to be worth automating: Debug, Clone, PartialEq, PartialOrd — that name again — and a handful of others. Every #[derive(...)] in every file you've written has been leaving behind precisely the shape of code this lesson just taught you to write with your own hands.
The big payoff: finishing CountError
Time for the third promise — the one with your name on it from two lessons ago. Here's CountError, exactly as you left it, gaining the three traits that lesson named, explained in one sentence each, and then deliberately set aside:
Take these one at a time — every one of them is something this lesson already showed you, wearing CountError's name instead of NewsArticle's or Tweet's.
impl fmt::Display for CountError — look at its shape before reading another word. It's describe, from two lessons ago, wearing a different name. Same match, same two arms, nearly the same messages — except where describe returned a String that something had to remember to call .describe() to see, write!(f, ...) writes directly into Rust's own formatting machinery. The instant this compiles, CountError gains the exact same superpower every number, string, and char has had since lesson one: drop a value straight into println!, no .describe(), no special-casing, ever again.
The two impl From<...> for CountError blocks: each one says, in code, exactly the sentence .map_err(CountError::Io) and .map_err(CountError::Parse) used to say by hand — "here is how a value of this type becomes a CountError." Write that sentence down once, here, as a trait implementation, and ? gains the ability to do that conversion everywhere, automatically, forever. This is, word for word, the mechanism three lessons ago described and then deliberately left unbuilt: ? "can paper over honest mismatches between error types, the moment a conversion path between them exists." These two blocks are that conversion path.
And then — impl std::error::Error for CountError {}. An entirely empty impl block, on purpose, and by now you know exactly what that means: a complete sentence, not a stub. Every method the Error trait defines comes with a default body, and the only two things it actually requires of a type are Debug and Display — and CountError already has both, one from #[derive(Debug)] since the day this type was born, the other from the block directly above this one. There's nothing left to say. CountError is now, formally, fully, an error — by the standard library's own definition of the word.
The moment
Here is read_count_from_file, after every trait above has been written:
Count the .map_err(...) calls. There are none. Not "fewer" — none. Both fallible lines now read exactly like every other ?-shortened line you've written since two lessons before this whole detour started — because, as far as the type system is concerned, there is genuinely nothing special about CountError left to work around. An io::Error meets a ? inside a function that returns Result<i32, CountError>; the compiler reaches for the From<io::Error> for CountError impl you wrote a moment ago, converts automatically, and moves on. Same story for the ParseIntError.
And main closes the very last loop. read_count_from_file(...)? sits inside a function returning Result<(), Box<dyn Error>> — the exact signature from three lessons ago, back when Box<dyn Error> was introduced as "some kind of error, and I genuinely don't need to know exactly which." dyn Error means precisely that: any type at all, decided at runtime, so long as it implements the Error trait. Two minutes ago, CountError became exactly that. ? converts it into the box automatically — the standard library ships a blanket conversion from any error type into Box<dyn Error>, and CountError now qualifies — and main never has to know, or care, that CountError exists at all.
Quick exercise
Pick a struct from an earlier exercise — Pair<T> from last lesson, or Guess from four lessons back, both work well — and give it impl fmt::Display, by hand, exactly the way CountError just got one. Then add #[derive(Debug)] to the same struct, and print the same value twice: once with the captured-identifier syntax you just wrote a body for, and once using the :? debug format from all the way back in the structs lesson. Two completely different outputs, from two completely different trait implementations, on the very same value — one you wrote yourself, one the compiler wrote for you, both precisely the same kind of thing.
Stop, for a second, and look at what just happened across this entire lesson. Three separate things that have been quietly sitting in the back of your mind — PartialOrd's mysterious bound from last lesson, #[derive(...)]'s quiet automation from practically the first lesson that used a struct, and CountError's slightly awkward .map_err() calls from two lessons ago — turn out to have been the exact same idea the entire time, seen from three different angles. One idea. Three outfits. That's traits.
One small thing was true of every function in this lesson that handed back a reference — Point::x from last lesson, largest from last lesson too: each one took exactly one reference as input. The compiler could always tell, without being told, "whatever you're handing back must be tied to the one reference I gave you — there's nothing else it could possibly be tied to." The very next lesson opens by breaking that pattern on purpose: a function that takes two references and hands back one of them — and, for the first time in this entire course, the compiler stops and asks you which one you meant. That question, and the small piece of syntax that answers it, is the last topic in "advanced" — and then this course turns toward building real things: lifetimes.