Rust learning: lessons, notes, and exercise crates
This commit is contained in:
@@ -0,0 +1,588 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>0007 — Files, io::Error, and FromStr</title>
|
||||
<link rel="stylesheet" href="../assets/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Files, <code>io::Error</code>, and <code>FromStr</code></h1>
|
||||
<p class="subtitle">Lesson 0007 · after <a href="0006-your-own-error-type.html">0006</a> · reading, then a 30-minute drill against a shipped test file · ~45 minutes</p>
|
||||
|
||||
<div class="callout">
|
||||
Every code block and every compiler message on this page was produced by running it today, in a scratch project.
|
||||
Nothing is written from memory. The demo domain is a weather log — your project is a task CLI, so nothing here
|
||||
pastes in. Translating is the work.
|
||||
</div>
|
||||
|
||||
<h2>Where 0006 left you, and the one loose end</h2>
|
||||
|
||||
<p>Twenty-four tests are green, and the error type behind them is real work: <code>TaskError</code> has seven
|
||||
variants, each with its own <code>Display</code> sentence, a <code>source()</code> that hands back the one wrapped
|
||||
cause, and an <code>impl From<ParseIntError></code>. That is the full set of obligations for a std-compatible
|
||||
error type, and you wrote all of it from the signatures up.</p>
|
||||
|
||||
<p>There is one loose end, and it is worth looking at closely before adding anything new. The <code>From</code>
|
||||
impl you wrote is never actually called. Over in <code>command.rs</code>, the conversion is still done by hand —
|
||||
once in the <code>done</code> arm, once in <code>remove</code>:</p>
|
||||
|
||||
<pre><code>let id: u32 = match id.parse() {
|
||||
Ok(n) => n,
|
||||
Err(e) => return Err(TaskError::BadId(e)), // this IS From::from, typed out
|
||||
};</code></pre>
|
||||
|
||||
<p>Compare that with what <code>From</code> exists to do. Your impl says "given a <code>ParseIntError</code>,
|
||||
build a <code>TaskError::BadId</code>" — which is exactly what the <code>Err</code> arm above says, in five lines
|
||||
instead of zero. The impl is correct; it simply never gets reached, because <code>match</code> handles the error
|
||||
before <code>?</code> would have had a chance to convert it. So the mechanism was learned and the reflex was not,
|
||||
which is the most common way a Rust concept half-lands.</p>
|
||||
|
||||
<p>Step 0 of today's drill deletes both blocks. Then today adds a <em>second</em> <code>From</code> impl, and this
|
||||
one you will not be able to route around by hand: the shape the code needs makes <code>?</code> the only
|
||||
reasonable option, and the compiler stops the build until the impl exists.</p>
|
||||
|
||||
<h2>Part 1 — A file is a <code>String</code> that can fail</h2>
|
||||
|
||||
<p>A save file is, from Rust's point of view, nothing more exotic than a <code>String</code> that might not arrive.
|
||||
Two functions in <a href="https://doc.rust-lang.org/std/fs/">std::fs</a> cover everything a CLI of this size needs.
|
||||
Each one opens the file, does the work, and closes it again, all inside the single call — there is no handle to keep
|
||||
track of and nothing to remember to close:</p>
|
||||
|
||||
<pre><code>fn read_to_string<P: AsRef<Path>>(path: P) -> io::Result<String>
|
||||
fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> io::Result<()></code></pre>
|
||||
<p class="cite">std: <a href="https://doc.rust-lang.org/std/fs/fn.read_to_string.html">fs::read_to_string</a> ·
|
||||
<a href="https://doc.rust-lang.org/std/fs/fn.write.html">fs::write</a></p>
|
||||
|
||||
<p>The return types look unfamiliar, so take them apart before going further.
|
||||
<code>io::Result<T></code> is not a new kind of <code>Result</code>; it is
|
||||
<a href="https://doc.rust-lang.org/std/io/type.Result.html">a type alias</a>, declared in std as
|
||||
<code>type Result<T> = std::result::Result<T, io::Error></code>. In other words the error half has already been
|
||||
filled in for you, because every function in that module fails the same way. Whenever you meet
|
||||
<code>io::Result<String></code> in a signature, read it silently as <code>Result<String, io::Error></code> and
|
||||
carry on — it is the same <code>Result</code> you have been matching on since chapter 9, and <code>?</code> works
|
||||
on it exactly as you would expect.</p>
|
||||
|
||||
<p>(The <code>P: AsRef<Path></code> in the signature is a convenience bound, and you can read past it for now.
|
||||
All it means is that you may pass a <code>&str</code>, a <code>String</code>, a <code>&Path</code>, or a
|
||||
<code>PathBuf</code>, and std will accept any of them. It is the same idea as the trait bounds from 0005, used to
|
||||
widen what a function will take.)</p>
|
||||
|
||||
<p>The interesting half is <code>io::Error</code>. Unlike <code>ParseIntError</code>, which really only means "that
|
||||
was not a number", an <code>io::Error</code> stands for dozens of distinct situations: the file does not exist, the
|
||||
path is a directory rather than a file, the process lacks permission to read it, the disk is full, the name is too
|
||||
long. All of those arrive as the same type, so the type alone cannot tell you what went wrong. You separate them by
|
||||
asking the value, using <a href="https://doc.rust-lang.org/std/io/enum.ErrorKind.html"><code>e.kind()</code></a>,
|
||||
which returns a variant of the <code>ErrorKind</code> enum:</p>
|
||||
|
||||
<pre><code>let missing = fs::read_to_string("/tmp/definitely-not-here.txt");
|
||||
println!("{:?}", missing.map_err(|e| e.kind()));</code></pre>
|
||||
<pre><code>missing -> Err(NotFound)</code></pre>
|
||||
|
||||
<p>Keep that <code>NotFound</code> in mind. It looks like a small detail, but it turns into the most important
|
||||
design decision of the whole lesson, and Part 4 comes back to it: on the very first run of your CLI, the save file
|
||||
legitimately does not exist yet, and how you treat that one kind decides whether a fresh install works or looks
|
||||
broken.</p>
|
||||
|
||||
<h2>Part 2 — The second <code>From</code>, and why it is legal</h2>
|
||||
|
||||
<p>You asked, after the last lesson, whether a type can have two <code>From</code> impls. The answer decides how
|
||||
today's code is shaped, so here is the rule stated precisely: <strong>for any given source type <code>T</code>,
|
||||
there may be exactly one <code>impl From<T> for YourType</code> in the whole program.</strong> Write a second
|
||||
one for the same <code>T</code> and the compiler stops you with
|
||||
<code>error[E0119]: conflicting implementations</code>.</p>
|
||||
|
||||
<p>The reason is worth holding on to, because it explains a lot of Rust's trait rules. At the moment you write
|
||||
<code>value?</code>, the only information the compiler has is the pair of types involved: it is converting a
|
||||
<code>ParseIntError</code> into a <code>TaskError</code>. Nothing at that call site records what you <em>meant</em>
|
||||
by the failure. If two impls existed for that pair, there would be two possible answers and no way to choose
|
||||
between them, so the language forbids the ambiguity up front rather than picking one for you.</p>
|
||||
|
||||
<p>Nothing stops you from adding an impl for a <em>different</em> source type, though, and that is what today
|
||||
needs — one conversion for parse failures, and a new one for file failures:</p>
|
||||
|
||||
<pre><code>impl From<ParseIntError> for TaskError { .. } // T = ParseIntError (0006)
|
||||
impl From<io::Error> for TaskError { .. } // T = io::Error (0007)</code></pre>
|
||||
|
||||
<p>Write <code>fs::write(path, out)?</code> before the impl exists and the compiler tells you exactly what is
|
||||
missing. Real output:</p>
|
||||
|
||||
<pre><code>error[E0277]: `?` couldn't convert the error to `TaskError`
|
||||
--> src/store.rs:82:29
|
||||
|
|
||||
82 | fs::write(path, out)?;
|
||||
| --------------------^ the trait `From<std::io::Error>` is not implemented for `TaskError`
|
||||
| |
|
||||
| this can't be annotated with `?` because it has type `Result<_, std::io::Error>`
|
||||
|
|
||||
note: `TaskError` needs to implement `From<std::io::Error>`
|
||||
= note: the question mark operation (`?`) implicitly performs a conversion
|
||||
on the error value using the `From` trait</code></pre>
|
||||
|
||||
<p>The final note in that message is the part worth reading twice. <code>?</code> has no special knowledge of
|
||||
<code>io</code>, and it is not a built-in shortcut for file handling: it simply calls <code>From::from</code> on
|
||||
whatever error it is given. It is the identical mechanism that has been quietly converting your
|
||||
<code>ParseIntError</code> into a <code>BadId</code> since 0006. Once you see <code>?</code> as "return early, and
|
||||
run the error through <code>From</code> on the way out", every one of these messages becomes predictable rather
|
||||
than mysterious.</p>
|
||||
|
||||
<h2>Part 3 — <code>FromStr</code>: the trait behind <code>.parse()</code></h2>
|
||||
|
||||
<p>You have been calling <code>.parse()</code> since the guessing game, and it has probably felt like a built-in
|
||||
piece of string handling. It is not. It is a trait method, and once you see the two declarations behind it, the
|
||||
whole thing stops being magic:</p>
|
||||
|
||||
<pre><code>impl str {
|
||||
pub fn parse<F: FromStr>(&self) -> Result<F, F::Err> { .. }
|
||||
}
|
||||
|
||||
pub trait FromStr: Sized {
|
||||
type Err; // an ASSOCIATED TYPE
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err>;
|
||||
}</code></pre>
|
||||
<p class="cite">std: <a href="https://doc.rust-lang.org/std/str/trait.FromStr.html">FromStr</a> ·
|
||||
Book: <a href="https://doc.rust-lang.org/stable/book/ch20-02-advanced-traits.html">20.2 — associated types</a></p>
|
||||
|
||||
<p>Read the two together. <code>"42".parse::<u32>()</code> works for exactly one reason: somewhere in std there
|
||||
is an <code>impl FromStr for u32</code>, and it declares <code>type Err = ParseIntError</code>. There is no
|
||||
special case for integers in the language. That means the door is open to you — implement the same trait for your
|
||||
own type and <code>.parse()</code> begins working on it immediately. This is a genuinely different move from
|
||||
writing a <code>Task::from_line</code> helper of your own. A helper is a function only your code knows about;
|
||||
implementing the trait means your type joins an interface that std and every other crate already speak, so any
|
||||
generic function taking <code>F: FromStr</code> will now accept a <code>Task</code> as well.</p>
|
||||
|
||||
<p>The unfamiliar line is <code>type Err;</code>, and it deserves its own paragraph because it is your first
|
||||
<strong>associated type</strong>. Think of it as a slot in the trait that the <em>implementor</em> fills in, once,
|
||||
and permanently — as opposed to a generic parameter, which the <em>caller</em> chooses at each call site. That
|
||||
distinction is exactly why <code>ParseIntError</code> appears nowhere in <code>parse()</code>'s signature: the
|
||||
signature says <code>F::Err</code>, meaning "whatever error type <code>F</code> declared when it implemented the
|
||||
trait". When you write <code>type Err = TaskError</code> in your impl, you are filling that slot for
|
||||
<code>Task</code>, and from then on <code>line.parse::<Task>()</code> is known to return
|
||||
<code>Result<Task, TaskError></code> without anyone having to say so again.</p>
|
||||
|
||||
<p>Leave the slot out and the compiler is explicit about the missing piece:</p>
|
||||
|
||||
<pre><code>error[E0046]: not all trait items implemented, missing: `Err`
|
||||
--> src/task.rs:103:1
|
||||
|
|
||||
103 | impl FromStr for Task {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ missing `Err` in implementation
|
||||
|
|
||||
= help: implement the missing item: `type Err = /* Type */;`</code></pre>
|
||||
|
||||
<p>And call <code>.parse::<Task>()</code> before implementing it at all:</p>
|
||||
|
||||
<pre><code>error[E0277]: the trait bound `Task: FromStr` is not satisfied
|
||||
--> src/store.rs:98:29
|
||||
|
|
||||
98 | tasks.push(line.parse::<Task>()?);
|
||||
| ^^^^^ unsatisfied trait bound
|
||||
|
|
||||
help: the trait `FromStr` is not implemented for `Task`</code></pre>
|
||||
|
||||
<p>The whole thing, in the weather-log demo — run today, output below:</p>
|
||||
|
||||
<pre><code>#[derive(Debug, PartialEq)]
|
||||
struct Reading { station: String, celsius: f64 }
|
||||
|
||||
impl FromStr for Reading {
|
||||
type Err = String; // your error type goes in the slot
|
||||
|
||||
fn from_str(line: &str) -> Result<Reading, String> {
|
||||
let (station, temp) =
|
||||
line.split_once('=').ok_or_else(|| line.to_string())?;
|
||||
Ok(Reading {
|
||||
station: station.to_string(),
|
||||
celsius: temp.parse().map_err(|_| line.to_string())?,
|
||||
})
|
||||
}
|
||||
}</code></pre>
|
||||
<pre><code>Ok(Reading { station: "oslo", celsius: -3.5 })
|
||||
Ok(Reading { station: "lagos", celsius: 31.0 })
|
||||
Err("broken")</code></pre>
|
||||
|
||||
<p>One line in there is doing something you have not seen before, so look at the inner
|
||||
<code>temp.parse().map_err(|_| ..)?</code> closely. It parses a <code>f64</code>, and the failure it can produce is
|
||||
a <code>ParseFloatError</code> — but notice what that failure <em>means</em> in this context. It does not mean "the
|
||||
user typed a bad number at the keyboard"; it means "the line stored in this file is corrupt". Those are two
|
||||
different problems, they deserve two different variants, and only one of them can be the one that <code>From</code>
|
||||
produces automatically.</p>
|
||||
|
||||
<p>So the shape to remember is this: <strong><code>?</code> on its own handles the single canonical conversion,
|
||||
and <code>map_err</code> is how you name a different variant for any other meaning of the same error type.</strong>
|
||||
In the drill you will write both, a few lines apart, on the very same <code>ParseIntError</code> — the CLI path
|
||||
keeps <code>?</code> and produces <code>BadId</code>, while the file path uses <code>map_err</code> and produces
|
||||
<code>BadLine</code>.</p>
|
||||
|
||||
<h2>Part 4 — Three decisions the tests will hold you to</h2>
|
||||
|
||||
<h3>A missing file is not an error</h3>
|
||||
|
||||
<p>Picture the very first time anyone runs your CLI. There is no <code>tasks.txt</code> yet, because nothing has
|
||||
ever created one. If <code>load</code> simply propagates whatever <code>fs::read_to_string</code> returns, that
|
||||
first run prints an error to stderr and exits 1 — the program looks broken before the user has done anything wrong.
|
||||
A missing file here is not a failure at all; it is the normal starting state, and it means "you have no tasks yet".</p>
|
||||
|
||||
<p>So this is one of the rare places where you deliberately catch a single kind of error and turn it into a
|
||||
successful result, while letting every other kind through untouched:</p>
|
||||
|
||||
<pre><code>let text = match fs::read_to_string(path) {
|
||||
Ok(text) => text,
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Store::new()),
|
||||
Err(e) => return Err(TaskError::Io(e)), // permissions etc. still fail
|
||||
};</code></pre>
|
||||
|
||||
<p>The new syntax is <code>Err(e) if ..</code>, which is called a <strong>match guard</strong>: an extra condition
|
||||
attached to an arm, so the arm only matches when the pattern fits <em>and</em> the condition holds. Here the first
|
||||
<code>Err</code> arm catches only <code>NotFound</code>, and anything else falls through to the arm below it.</p>
|
||||
|
||||
<p>It is worth being clear about what this is not, because the lazy version is tempting. This is not
|
||||
<code>unwrap_or_default()</code> and it is not <code>.ok()</code>. Both of those would treat <em>every</em> io
|
||||
failure as "no tasks" — so a permissions problem, or a disk that has gone read-only, would silently present the
|
||||
user with an empty list, and the next <code>save</code> would overwrite their real file with nothing. One specific
|
||||
kind of failure is expected; the rest genuinely are failures and must still be reported.</p>
|
||||
|
||||
<h3>The saved format is not the <code>Display</code> format</h3>
|
||||
|
||||
<p>You already have a <code>Display for Task</code> from lesson 0005, and it prints
|
||||
<code>1 [done] buy milk (high)</code>. That is a good sentence for a person reading a terminal, and it is a poor
|
||||
format to read back in: to reconstruct the task you would have to find the brackets and parentheses, while allowing
|
||||
for a title that might itself contain either. The format fights you because it was never designed to be parsed.</p>
|
||||
|
||||
<p>Storage has different requirements from presentation, so give it its own format — one with a separator that
|
||||
splits cleanly and a fixed field order:</p>
|
||||
|
||||
<pre><code>1|done|high|buy milk</code></pre>
|
||||
|
||||
<p>Two audiences, two formats: <code>Display</code> stays exactly as it is for the <code>list</code> command, and
|
||||
<code>to_line</code> is added beside it for the file. Do not be tempted to make one serve both.</p>
|
||||
|
||||
<p>Notice also that the title is placed <em>last</em>. That is deliberate, and it lets the title contain anything at
|
||||
all, including the separator itself. The reason is how <code>splitn</code> works:
|
||||
<code>"a|b|c|d|e".splitn(4, '|')</code> stops splitting after it has produced four pieces, so the fourth piece is
|
||||
the entire remainder, <code>"d|e"</code>, with its <code>|</code> intact. Reach for <code>split</code> instead and a
|
||||
title containing a pipe silently loses everything after it. One of the shipped tests covers exactly this case.</p>
|
||||
|
||||
<h3><code>#[derive(PartialEq)]</code> will break, and that is informative</h3>
|
||||
|
||||
<p>Add <code>Io(io::Error)</code> to the enum and the derive on line 3 fails:</p>
|
||||
|
||||
<pre><code>error[E0369]: binary operation `==` cannot be applied to type `&std::io::Error`
|
||||
--> src/error.rs:12:8
|
||||
|
|
||||
3 | #[derive(Debug, PartialEq)]
|
||||
| --------- in this derive macro expansion
|
||||
...
|
||||
12 | Io(std::io::Error),
|
||||
| ^^^^^^^^^^^^^^
|
||||
|
|
||||
note: `std::io::Error` does not implement `PartialEq`</code></pre>
|
||||
|
||||
<p>The note at the bottom is the interesting part: <code>io::Error</code> deliberately does not implement
|
||||
<code>PartialEq</code>. That is a considered decision by the std authors, not an oversight — two io failures can
|
||||
carry the same message and still come from entirely different OS state, so "are these two errors equal?" has no
|
||||
honest answer. Your enum now contains one, and equality for the whole enum is therefore no longer derivable.</p>
|
||||
|
||||
<p>This is also a useful moment to see what <code>derive</code> actually is. It is not a language feature attached
|
||||
to the type; it is a code generator that writes an ordinary <code>impl</code> for you, comparing every field with
|
||||
<code>==</code>. When one field cannot be compared, the generated line does not compile, and you get the error
|
||||
above pointing at the derive itself.</p>
|
||||
|
||||
<p>Dropping <code>PartialEq</code> is not an option, because the 0006 tests compare <code>TaskError</code> values
|
||||
with <code>assert_eq!</code> and you may not edit them. So write the impl by hand instead. This part is mechanical
|
||||
rather than conceptual — type it, understand the three notes underneath, and move on:</p>
|
||||
|
||||
<pre><code>impl PartialEq for TaskError {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
use TaskError::*;
|
||||
match (self, other) {
|
||||
(UnknownCommand(a), UnknownCommand(b))
|
||||
| (BadPriority(a), BadPriority(b))
|
||||
| (BadLine(a), BadLine(b)) => a == b,
|
||||
(BadId(a), BadId(b)) => a == b,
|
||||
(NotFound(a), NotFound(b)) => a == b,
|
||||
(Io(a), Io(b)) => a.kind() == b.kind(), // the kind, not the error
|
||||
_ => std::mem::discriminant(self) == std::mem::discriminant(other),
|
||||
}
|
||||
}
|
||||
}</code></pre>
|
||||
|
||||
<p>Three pieces of that impl are new, and each is useful well beyond this one function.</p>
|
||||
|
||||
<p>First, <code>match (self, other)</code> matches on a <strong>tuple of two values at once</strong>. You build a
|
||||
temporary pair and pattern-match both halves together, which is how you ask "are these the same variant, and if so,
|
||||
are their payloads equal?" in a single expression.</p>
|
||||
|
||||
<p>Second, <code>(A(a), A(b)) | (B(a), B(b)) =></code> is an <strong>or-pattern</strong>: several patterns
|
||||
sharing one arm. Rust allows it here because every alternative binds the same names, <code>a</code> and
|
||||
<code>b</code>, at the same types, so the arm's body is valid whichever alternative matched. That is what lets three
|
||||
<code>String</code>-carrying variants share a single line instead of taking three.</p>
|
||||
|
||||
<p>Third, <a href="https://doc.rust-lang.org/std/mem/fn.discriminant.html"><code>mem::discriminant</code></a>
|
||||
returns an opaque value identifying <em>which</em> variant a value is, ignoring any payload. Comparing two of them
|
||||
answers "same variant?" without your having to name the variants at all, which handles the four payload-free cases
|
||||
in one line.</p>
|
||||
|
||||
<p>That last convenience has a real cost, and it is the kind of thing to notice now rather than discover later. The
|
||||
<code>_</code> arm means the compiler will never again force you to update this impl when you add a variant — and a
|
||||
new variant carrying data would then be compared by variant alone, treating two different payloads as equal. It is
|
||||
an acceptable trade for a small error type, but it is a trade, not a free win.</p>
|
||||
|
||||
<h2>Check yourself before the drill</h2>
|
||||
|
||||
<p>Six questions before you touch the keyboard. Try to answer each one out loud, in full sentences, before you
|
||||
reveal or click — an answer you can say is an answer you have understood, and one you can only recognise on a page
|
||||
usually is not. Getting one wrong here costs you nothing; getting the same thing wrong twenty minutes into the
|
||||
drill costs you the drill.</p>
|
||||
|
||||
<div class="q" data-type="mcq" data-topic="Traits">
|
||||
<p class="topic">Traits</p>
|
||||
<p class="prompt">You have <code>impl From<ParseIntError> for TaskError</code>. Which second impl is rejected by the compiler?</p>
|
||||
<div class="options">
|
||||
<button class="opt" data-correct="true">A second <code>From<ParseIntError> for TaskError</code></button>
|
||||
<button class="opt" data-correct="false">An added <code>From<io::Error> for TaskError</code></button>
|
||||
<button class="opt" data-correct="false">An added <code>From<ParseIntError> for LineError</code></button>
|
||||
<button class="opt" data-correct="false">An added <code>From<ParseFloatError> for TaskError</code></button>
|
||||
</div>
|
||||
<div class="explain hidden">One impl per (trait, source type, target type). Two impls for the same <code>T</code> give <code>error[E0119]: conflicting implementations</code>, because <code>?</code> would have no single answer for what to convert into. Changing either the source type or the target type makes it a different impl, so the other three are all legal.</div>
|
||||
</div>
|
||||
|
||||
<div class="q" data-type="recall" data-topic="Traits">
|
||||
<p class="topic">Traits</p>
|
||||
<p class="prompt">What is <code>type Err</code> in <code>impl FromStr</code>, and why is it not written <code>impl FromStr<Err></code>?</p>
|
||||
<button class="reveal-btn">Show answer</button>
|
||||
<div class="answer hidden">It is an <strong>associated type</strong>: a slot in the trait that the implementor fills in exactly once. A generic parameter is chosen by the <em>caller</em> and lets one type implement the trait many times; an associated type is chosen by the <em>implementor</em>, so <code>Task</code> implements <code>FromStr</code> once and <code>"…".parse::<Task>()</code> is never ambiguous about which error comes back. Omit it and you get <code>error[E0046]: missing Err in implementation</code>.</div>
|
||||
<div class="grade hidden">
|
||||
<button data-grade="hit">Got it</button>
|
||||
<button data-grade="miss">Missed it</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q" data-type="mcq" data-topic="Error handling">
|
||||
<p class="topic">Error handling</p>
|
||||
<p class="prompt">Your <code>load</code> calls <code>fs::read_to_string(path)?</code> and the file does not exist yet. What does the user see on their first ever run?</p>
|
||||
<div class="options">
|
||||
<button class="opt" data-correct="false">An empty task list, printed normally</button>
|
||||
<button class="opt" data-correct="true">An error on stderr, and exit code 1</button>
|
||||
<button class="opt" data-correct="false">A new empty file, created silently</button>
|
||||
<button class="opt" data-correct="false">A panic with a full stack backtrace</button>
|
||||
</div>
|
||||
<div class="explain hidden">A bare <code>?</code> propagates every <code>io::Error</code>, including <code>ErrorKind::NotFound</code>. Your <code>main</code> prints it to stderr and exits 1 — so a brand-new install looks broken. That is why <code>load</code> needs the match guard <code>Err(e) if e.kind() == ErrorKind::NotFound => Ok(Store::new())</code>, and why the shipped test <code>a_missing_file_is_an_empty_store_not_an_error</code> exists.</div>
|
||||
</div>
|
||||
|
||||
<div class="q" data-type="recall" data-topic="Error handling">
|
||||
<p class="topic">Error handling</p>
|
||||
<p class="prompt">Both <code>done abc</code> (a CLI argument) and a corrupt saved line produce a <code>ParseIntError</code>. You want two different variants. How, given only one <code>From</code> impl is allowed?</p>
|
||||
<button class="reveal-btn">Show answer</button>
|
||||
<div class="answer hidden"><code>?</code> for the canonical one, <code>map_err</code> for the other. The CLI path keeps <code>id.parse()?</code>, which calls <code>From</code> and yields <code>BadId</code>. The file path writes <code>part.parse().map_err(|_| TaskError::BadLine(line.to_string()))?</code>, choosing the variant explicitly. Same error type, two meanings, and the meaning is a property of the call site, not of the type — which is exactly what <code>From</code> cannot express.</div>
|
||||
<div class="grade hidden">
|
||||
<button data-grade="hit">Got it</button>
|
||||
<button data-grade="miss">Missed it</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q" data-type="recall" data-topic="Ownership">
|
||||
<p class="topic">Ownership</p>
|
||||
<p class="prompt"><code>fn load(path: &Path) -> Result<Store, TaskError></code> — no <code>&self</code>, and it returns a <code>Store</code> by value. Why is that not a copy, and where does the returned value live?</p>
|
||||
<button class="reveal-btn">Show answer</button>
|
||||
<div class="answer hidden">It is an <strong>associated function</strong>, not a method — no receiver, called as <code>Store::load(&path)</code>, the same shape as <code>Store::new()</code>. Returning by value <em>moves</em> ownership to the caller; the <code>Vec</code>'s heap buffer is never copied, only the three-word handle. <code>&Path</code> is borrowed because <code>load</code> only reads the path and the caller keeps it for the later <code>save</code>.</div>
|
||||
<div class="grade hidden">
|
||||
<button data-grade="hit">Got it</button>
|
||||
<button data-grade="miss">Missed it</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q" data-type="recall" data-topic="Enums">
|
||||
<p class="topic">Enums</p>
|
||||
<p class="prompt">After loading two tasks from a file, why must <code>Store</code> recompute its next id instead of starting at 1?</p>
|
||||
<button class="reveal-btn">Show answer</button>
|
||||
<div class="answer hidden">Because the counter is state that lives in memory only, and the file does not save it. Load without recomputing and the next <code>add</code> hands out id 1 again — two tasks share an id, and <code>done 1</code> silently completes the wrong one. The fix is one line: <code>tasks.iter().map(|t| t.id).max().unwrap_or(0) + 1</code>. The test <code>ids_do_not_restart_after_a_reload</code> is exactly this bug, caught.</div>
|
||||
<div class="grade hidden">
|
||||
<button data-grade="hit">Got it</button>
|
||||
<button data-grade="miss">Missed it</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="summary"><div id="summary-body"></div><p id="summary-total"></p>
|
||||
<button id="report-btn">Copy report</button><pre id="report-output" class="hidden"></pre></div>
|
||||
|
||||
<h2>The drill — 30 minutes, your own crate</h2>
|
||||
|
||||
<p>Type it, do not paste it. The weather log above is a different program. Keep the
|
||||
<a href="../reference/rust-syntax.html#files">files & FromStr reference</a> open — looking syntax up is free.</p>
|
||||
|
||||
<pre><code>cd ~/learn-rust/tasks
|
||||
cp ../lessons/0007-persist-spec.rs tests/persist.rs
|
||||
cargo test # 8 new tests fail to compile — that is the starting line</code></pre>
|
||||
|
||||
<p>Do not edit anything in <code>tests/</code>. All 24 existing tests must still pass. Target at the end:
|
||||
<strong>32 passing</strong>.</p>
|
||||
|
||||
<h3>Step 0 — pay off 0006 (2 minutes)</h3>
|
||||
|
||||
<p>Delete both <code>match id.parse()</code> blocks in <code>command.rs</code>. Each becomes one line, and the
|
||||
<code>From</code> impl you wrote last lesson finally does its job:</p>
|
||||
|
||||
<pre><code>let id: u32 = args.get(1).ok_or(TaskError::MissingId)?.parse()?;</code></pre>
|
||||
|
||||
<p><strong>Check:</strong> <code>grep -c "match id.parse" src/command.rs</code> prints <code>0</code>, and
|
||||
<code>cargo test --test errors</code> still passes 7.</p>
|
||||
|
||||
<h3>Step 1 — two new variants, and a hand-written <code>PartialEq</code></h3>
|
||||
|
||||
<p>Add to <code>TaskError</code>:</p>
|
||||
|
||||
<pre><code>BadLine(String), // a saved line that cannot be read back — carries the line
|
||||
Io(io::Error), // the file could not be read or written — wraps std's error</code></pre>
|
||||
|
||||
<p>Then: remove <code>PartialEq</code> from the derive and write the impl from Part 4; add both
|
||||
<code>Display</code> arms; add <code>Io(e) => Some(e)</code> to <code>source()</code>; add
|
||||
<code>From<io::Error></code>. Exact messages, checked by the tests:</p>
|
||||
|
||||
<table>
|
||||
<tr><th>Variant</th><th><code>to_string()</code> must be</th></tr>
|
||||
<tr><td><code>BadLine("rubbish")</code></td><td><code>cannot read saved line: rubbish</code></td></tr>
|
||||
<tr><td><code>Io(..)</code></td><td><code>cannot read or write the task file</code></td></tr>
|
||||
</table>
|
||||
|
||||
<p><strong>Check:</strong> <code>cargo build</code> passes, and <code>cargo test --test errors</code> is still 7/7
|
||||
— the old variants must behave exactly as before.</p>
|
||||
|
||||
<h3>Step 2 — <code>task.rs</code>: one line out, one line in</h3>
|
||||
|
||||
<p>Three additions:</p>
|
||||
<ul>
|
||||
<li><code>Status::parse(&str) -> Option<Status></code> — the mirror of <code>label()</code>, same shape as
|
||||
<code>Priority::parse</code>, which you already have.</li>
|
||||
<li><code>Task::to_line(&self) -> String</code> — <code>id|status|priority|title</code>.</li>
|
||||
<li><code>impl FromStr for Task</code> with <code>type Err = TaskError</code>, using
|
||||
<code>splitn(4, '|')</code>. Every failure is <code>BadLine(line.to_string())</code> — including the id, which is
|
||||
where <code>map_err</code> earns its keep.</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Check:</strong> <code>cargo test --test persist a_task_becomes</code> and
|
||||
<code>cargo test --test persist a_title_may</code> both pass.</p>
|
||||
|
||||
<details>
|
||||
<summary>Stuck on repeating <code>BadLine(line.to_string())</code> five times?</summary>
|
||||
<p>Bind it once as a closure and hand it to <code>ok_or_else</code>: <code>let bad = || TaskError::BadLine(line.to_string());</code>
|
||||
then <code>parts.next().ok_or_else(bad)?</code>. Note <code>ok_or_else</code>, not <code>ok_or</code> — the first
|
||||
takes a closure and only builds the error when there is one, the second builds it every time. With a
|
||||
<code>String</code> allocation inside, that difference is real.</p>
|
||||
</details>
|
||||
|
||||
<h3>Step 3 — <code>store.rs</code>: <code>save</code> and <code>load</code></h3>
|
||||
|
||||
<pre><code>pub fn save(&self, path: &Path) -> Result<(), TaskError>
|
||||
pub fn load(path: &Path) -> Result<Store, TaskError> // associated fn</code></pre>
|
||||
|
||||
<p><code>save</code> builds one <code>String</code> — one line per task, each ending in <code>\n</code> — and calls
|
||||
<code>fs::write</code> once. <code>load</code> reads, skips empty lines, parses each into a <code>Task</code>, and
|
||||
recomputes the next id. Both the missing-file guard and the id recomputation are in Part 4.</p>
|
||||
|
||||
<p><strong>Check:</strong> <code>cargo test --test persist</code> passes all 8.</p>
|
||||
|
||||
<h3>Step 4 — <code>main.rs</code>: load, act, save</h3>
|
||||
|
||||
<p>Where the file lives should not be hard-coded into the logic. One line of ch12 gets you an override for free:</p>
|
||||
|
||||
<pre><code>let path = PathBuf::from(
|
||||
env::var("TASKS_FILE").unwrap_or_else(|_| "tasks.txt".to_string()));</code></pre>
|
||||
|
||||
<p>Then <code>run</code> takes the path, loads the store itself, and saves at the end. Note the ordering that falls
|
||||
out of <code>?</code>: an error anywhere means <code>save</code> is never reached, so a failed command cannot
|
||||
corrupt the file.</p>
|
||||
|
||||
<p><strong>Check — your CLI now remembers things:</strong></p>
|
||||
|
||||
<pre><code>$ cd $(mktemp -d) # empty dir: proves a first run works with no file
|
||||
$ run(){ TASKS_FILE=t.txt cargo run -q --manifest-path ~/learn-rust/tasks/Cargo.toml -- "$@"; }
|
||||
$ run add "buy milk" high
|
||||
added task 1
|
||||
$ run add "call bank"
|
||||
added task 2
|
||||
$ run done 1
|
||||
completed 1
|
||||
$ run list
|
||||
1 [done] buy milk (high)
|
||||
2 [todo] call bank (medium)
|
||||
$ cat t.txt
|
||||
1|done|high|buy milk
|
||||
2|todo|medium|call bank
|
||||
$ echo "rubbish" >> t.txt ; run list ; echo $?
|
||||
error: cannot read saved line: rubbish
|
||||
1</code></pre>
|
||||
|
||||
<p>That last one is the payoff for <code>BadLine(String)</code> carrying the line: the message names the exact
|
||||
text to go and fix. A <code>String</code> error, or a bare <code>Io</code>, could not.</p>
|
||||
|
||||
<p><strong>Final check:</strong> <code>cargo test</code> → 17 + 7 + 8 = <strong>32 passed</strong>. And add
|
||||
<code>tasks.txt</code> to <code>.gitignore</code> — it is user data, not source.</p>
|
||||
|
||||
<h3>Then stop</h3>
|
||||
|
||||
<p>Not today: <code>serde</code> and JSON (the real answer for storage, but it teaches a crate rather than a
|
||||
concept), file locking, and <code>BufReader</code> for files too big to hold in memory. Your task file is a few
|
||||
kilobytes; <code>read_to_string</code> is the correct tool, and reaching for a buffered reader here would be
|
||||
copying a pattern you do not need.</p>
|
||||
|
||||
<h2>What this closed</h2>
|
||||
|
||||
<p>Chapter 12 moves to <em>produced</em> on the <a href="../reference/book-coverage.html">coverage map</a>:
|
||||
<code>env::args</code>, <code>env::var</code>, <code>fs</code>, stderr, and exit codes are now all in your own
|
||||
code. Two gaps left before the job-ready floor, and they are next:</p>
|
||||
|
||||
<ol>
|
||||
<li><strong>ch 8 + 13 — <code>HashMap</code>, <code>map</code>/<code>filter</code>/<code>collect</code></strong>,
|
||||
plus your first hand-written generic function (lesson 0008). Your <code>load</code> loop is a
|
||||
<code>collect::<Result<Vec<_>, _>>()</code> waiting to happen — I left it as a <code>for</code> loop
|
||||
deliberately so 0008 has something of yours to rewrite.</li>
|
||||
<li><strong>ch 11 — writing your own tests</strong>: you have now consumed 32 of mine and written zero
|
||||
(lesson 0009).</li>
|
||||
</ol>
|
||||
|
||||
<h2>Take it outside</h2>
|
||||
|
||||
<p>The forum ask from 0006 still stands and is now stronger: <code>error.rs</code> holds both
|
||||
<em>user mistakes</em> (<code>UnknownCommand</code>, <code>BadPriority</code>) and <em>system failures</em>
|
||||
(<code>Io</code>, <code>BadLine</code>) in one enum. Many Rust developers would split those into two types. Post it
|
||||
on <a href="https://users.rust-lang.org">users.rust-lang.org</a> (Code Review category) and ask which they would
|
||||
do and why. That is a genuine design question with real disagreement behind it — the answer you get back is wisdom
|
||||
you cannot derive from the book.</p>
|
||||
|
||||
<h2>The five sentences worth keeping</h2>
|
||||
|
||||
<ol>
|
||||
<li><code>io::Result<T></code> is just <code>Result<T, io::Error></code>; <code>e.kind()</code> is how you
|
||||
tell one io failure from another.</li>
|
||||
<li>One <code>impl From<T> for YourError</code> per <code>T</code> — a different <code>T</code> is a new impl,
|
||||
a repeat <code>T</code> is <code>E0119</code>.</li>
|
||||
<li><code>?</code> for the canonical conversion, <code>map_err</code> for every other meaning of the same error.</li>
|
||||
<li><code>FromStr</code> is what <code>.parse()</code> calls; <code>type Err</code> is an associated type — a slot
|
||||
the implementor fills, not a parameter the caller passes.</li>
|
||||
<li>A missing file on first run is expected, not exceptional: match <code>ErrorKind::NotFound</code>, and let every
|
||||
other kind fail loudly.</li>
|
||||
</ol>
|
||||
|
||||
<footer>
|
||||
<p><strong>Primary source:</strong> The Rust Book
|
||||
<a href="https://doc.rust-lang.org/stable/book/ch12-00-an-io-project.html">chapter 12 — An I/O Project</a>, and
|
||||
specifically <a href="https://doc.rust-lang.org/stable/book/ch12-02-reading-a-file.html">12.2 reading a file</a>
|
||||
and <a href="https://doc.rust-lang.org/stable/book/ch12-05-working-with-environment-variables.html">12.5
|
||||
environment variables</a>. Then the one-screen
|
||||
<a href="https://doc.rust-lang.org/std/str/trait.FromStr.html">std::str::FromStr</a> page — read the
|
||||
<code>Point</code> example there, it is the same shape as your <code>Task</code>.</p>
|
||||
<p>Previous: <a href="0006-your-own-error-type.html">0006 — Your own error type</a> ·
|
||||
<a href="0005-traits-display-and-errors.html">0005 — Traits, Display, errors</a> ·
|
||||
<a href="0004-structs-enums-packages.html">0004 — Structs, enums, packages</a><br />
|
||||
Reference: <a href="../reference/rust-syntax.html#files">Files & FromStr</a> ·
|
||||
<a href="../reference/rust-syntax.html#error-types">Custom error types</a> ·
|
||||
<a href="../reference/book-coverage.html">Coverage map</a></p>
|
||||
<p><strong>Ask me things.</strong> Bring the compiler output verbatim — step 1 breaks the derive and step 2 is the
|
||||
first trait you have implemented with an associated type. If a paragraph did not land, name it; that is my fault to
|
||||
fix, and cheaper to fix now than mid-drill.</p>
|
||||
</footer>
|
||||
|
||||
<script src="../assets/quiz.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user