Rust learning: lessons, notes, and exercise crates
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
# Recognition without production: ch1–9 is recall-only, writing ability is gone
|
||||
|
||||
Diagnostic (lesson 0001, 18 questions across ch1–9) scored 8/18, but the decisive signal was the user's own report: *"I even forgot how to write the code."* Recognition of concepts is partially intact; **production from a blank file is not**. Future lessons must be typing-first — quizzes measure this gap but do not close it.
|
||||
|
||||
**Evidence** — self-graded diagnostic, 2026-08-28:
|
||||
- Weak: Ownership 1/2, Error Handling 1/2, Data Types 0/1, References & Borrowing 1/2, Enums & Pattern Matching 1/2, Common Collections 1/2, Modules & Paths 0/1, Control Flow 0/1
|
||||
- Solid: Variables 1/1, Structs 2/2, Functions 1/1, Slices 1/1
|
||||
|
||||
**Implications**
|
||||
- Every lesson from 0002 on must have the user typing real code in a real `cargo` project, with `cargo run` / `cargo test` as the feedback loop. Pure quiz lessons are diagnostic instruments only.
|
||||
- The user explicitly asked to cover "solid" topics too — treat the diagnostic as weighting, not as a filter. Solid topics get folded into lessons as supporting material rather than skipped.
|
||||
- "Solid" results here are low-confidence: 1/1 and 2/2 samples, self-graded, on recognition-style questions. Do not treat Structs/Functions/Slices as owned until seen in produced code.
|
||||
- A syntax reference was the missing prerequisite — forgotten syntax was consuming the working memory needed for concepts. `reference/rust-syntax.html` now exists and should be linked from every lesson.
|
||||
- Modules & Paths (0/1) is untouched by lesson 0002 beyond `mod tests`. It needs its own lesson, and the user's existing `restauran`/`learn-modules` projects are the natural material.
|
||||
@@ -0,0 +1,5 @@
|
||||
# A spec of signatures is not a spec; concepts must precede the project
|
||||
|
||||
Lesson 0003 shipped as type signatures plus a test suite and the user could not start: "you don't even have description about the specification, so i don't know what to make." Signatures answer *what the compiler will accept*, not *what the program is for*. A spec-driven lesson needs a plain-language description of the program's purpose, its data, and its commands **before** any signature appears — and the description should be written so the type choices fall out of it ("a task is an id AND a title AND a status" → struct; "a status is one of three" → enum), letting the learner derive the model instead of reading it off a contract.
|
||||
|
||||
The user also asked to be taught structs, enums, and packages as if from zero, having previously said they were weak on them. This corrects an earlier assumption: the diagnostic's per-topic scores (Structs 2/2, Enums 1/2) overstated real understanding, because recognition-style questions can be passed without being able to *design* with the concept. Self-reported weakness outranks diagnostic scores. Lesson 0004 was written to fill this, and the ordering rule going forward is: concept lesson (knowledge, reading, real compiler output) → project lesson (skill, typing, tests) — never a project alone for a topic the user has named as weak.
|
||||
@@ -0,0 +1,15 @@
|
||||
# 0003 landed: structs/enums/packages are owned; the untested file is where the gap moved
|
||||
|
||||
Lesson 0003 came back with 17/17 tests green and code that is genuinely idiomatic: `?` with `ok_or`, `iter().find(|task| task.id == id)` with a closure, `iter_mut()` to mutate in place, a private `Vec<Task>` behind `pub fn tasks(&self) -> &[Task]`. Structs, enums with data, `impl`, the two-crate package and cross-module `crate::` paths can be treated as **produced, not just recognised** — the first topics in this workspace to earn that. Self-reported weakness on those three is resolved.
|
||||
|
||||
**The gap moved to the file no test could reach.** `src/main.rs` violates the spec's error contract in three ways, all confirmed by running the crate:
|
||||
|
||||
- `cargo run -- fly` → message on **stdout**, exit **0** (spec: stderr, exit 1)
|
||||
- `cargo run -- done 9` → `.unwrap()` panic, exit **101**
|
||||
- errors formatted by hand in `main` instead of by the type
|
||||
|
||||
This is exactly the mission's "errors with `Result`, not `panic!`", and it went unnoticed because the 17 tests are integration tests against the library crate — by design they cannot see `main.rs`. **Implication for future specs: any behaviour stated in prose but unreachable by the test suite will not get done.** Either test it (a spec test shelling out to the binary) or make it the explicit drill of the following lesson. Lesson 0005 takes the second route, deliberately, because the fix needs traits.
|
||||
|
||||
**Sequencing decision.** Traits were chosen over collections/iterators or async as the next topic, because: the user's own code now has two visible trait-shaped holes (hand-built display strings, `String` errors), every backend crate they will meet (serde, axum, tokio) is trait-driven, and ch10 is the next unread chapter. Per LR-0002's ordering rule, 0005 is the concept lesson; the `String` → `TaskError` enum conversion is the project lesson (0006) and is explicitly deferred in 0005's text so the drill stays inside working memory.
|
||||
|
||||
**Format note that worked and should continue:** the drill in 0005 targets the crate the user already wrote, with a shell-level check per step (`echo $?`) rather than a new test file. Feedback is immediate, and the reward is their own project getting better rather than a throwaway exercise. Reference impl proved in a temp copy first: 17/17 still pass after the drill's three steps.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Traits are produced, not just recognised — and the CLI contract finally landed
|
||||
|
||||
The 0005 drill was completed on the user's own `tasks` crate: `impl fmt::Display for Task` written from the signature
|
||||
up, `unwrap` removed from `main.rs`, `run(args, store) -> Result<(), String>` extracted, `Command::parse(args)?`
|
||||
matching on `Command` values, and `eprintln!` + `process::exit(1)` at the edge. All three shell checks pass
|
||||
(`fly` → exit 1, `done 9` → exit 1, `add "buy milk" 2>/dev/null` → exit 0) and the 17 spec tests still pass.
|
||||
Traits, `Display`, and `?` can be treated as owned from here — lesson 0006 assumes them instead of teaching them.
|
||||
|
||||
## Evidence
|
||||
Verified by running the drill's own checks against `~/learn-rust/tasks`, not by reading the diff.
|
||||
The `?`-on-`parse` shape was wrong on first submission (`match Command::parse(&args) { … Err(e) => Err(e) }`) and
|
||||
was corrected after review, so `?` is now produced but was not the first instinct — worth one more forced repetition
|
||||
in 0006, where the error type changes under five call sites at once.
|
||||
|
||||
## Implications
|
||||
- The stderr/exit-1 contract has now been missed twice before landing (0003 spec, 0005 step 3). The pattern is
|
||||
clear: a step whose check the user does not actually paste into a shell does not get done. Every future drill step
|
||||
needs a one-line runnable check, and the lesson should say "run it, do not eyeball it".
|
||||
- The recall quiz scored Enums 0/1 while the same session produced three working enums. Recall of *why* a construct
|
||||
exists lags the ability to use it. Fix by making the compiler state the reason (0006 shows real `E0004` output for
|
||||
a newly added variant) rather than asserting it in prose.
|
||||
- Their own duplicated `"id not found"` string across `command.rs` and `store.rs` is the concrete motivation for
|
||||
`TaskError`; using the user's own defect beats an invented example.
|
||||
@@ -0,0 +1,38 @@
|
||||
# A trait impl can be written correctly and still not be reached for
|
||||
|
||||
The 0006 drill landed 24/24 with the shipped spec unedited: seven `TaskError` variants, correct `Display`
|
||||
sentences, `source()` returning the wrapped `ParseIntError` for exactly one variant, and
|
||||
`impl From<ParseIntError> for TaskError`. Every trait obligation was produced from the signature up.
|
||||
|
||||
And the `From` impl was never called. `command.rs` still converted by hand, twice:
|
||||
|
||||
```rust
|
||||
let id: u32 = match id.parse() { Ok(n) => n, Err(e) => return Err(TaskError::BadId(e)) };
|
||||
```
|
||||
|
||||
That is `From::from` typed out longhand. The tests pass either way, so nothing in the feedback loop objected.
|
||||
|
||||
## Evidence
|
||||
|
||||
Read against the real crate, plus `cargo clippy`: 24 green, `tests/errors.rs` byte-identical to
|
||||
`lessons/0006-errors-spec.rs`, two duplicated `match id.parse()` blocks in `command.rs`, and the same
|
||||
`match … Ok(_) => ()` in `main.rs` that LR-0004 flagged after 0005 (clippy's `single_match`).
|
||||
|
||||
The follow-up questions were the more useful signal. All three were about the *mechanism*, not the syntax:
|
||||
can two `From` impls exist for one type, where does the wrapped message go, does a developer walk `source()`
|
||||
by hand every time. The syntax was owned; the model of what the syntax buys was not.
|
||||
|
||||
## Implications
|
||||
|
||||
- **A drill step needs a check that fails when the point is missed.** "Both `match` blocks collapse to
|
||||
`id.parse()?`" was written in the 0006 prose and had no check beside it, so it did not happen — the same
|
||||
failure mode as the stderr/exit-1 contract in LR-0004, now seen three times. 0007 gives it a grep check
|
||||
(`grep -c "match id.parse" src/command.rs` → `0`) as step 0.
|
||||
- **Force the impl, do not suggest it.** In 0007 the second conversion (`From<io::Error>`) cannot be hand-rolled
|
||||
around without the compiler complaining, because `?` on `fs::write` is the only reasonable shape. `E0277`
|
||||
does the teaching that prose could not.
|
||||
- **Recall of "why" still lags production.** Same pattern as the 0005 quiz (Enums 0/1 while writing three enums).
|
||||
The fix that works is showing real compiler output for the failure mode, not asserting the rule — so 0007
|
||||
ships `E0119`, `E0277`, `E0046`, and `E0369`, each captured by running it.
|
||||
- The user asks precise mechanism questions when given room to. Budget for them: leaving the ladder of
|
||||
`?` → `From` → `source()` → `anyhow` explicit in the lesson costs less than answering it four times after.
|
||||
@@ -0,0 +1,47 @@
|
||||
# A green suite says nothing about the surface it cannot reach
|
||||
|
||||
The 0008 drill landed 46/46 with the shipped spec unedited. The library half is genuinely produced:
|
||||
`tally<T, K, F>` written from its signature and used at three `T`/`K` pairs, `Store::load` as one
|
||||
`collect::<Result<Vec<Task>, TaskError>>()?`, `remove_completed` on `Vec::retain`, `titles_with` as a
|
||||
`filter`/`map`/`collect` chain, `count_by_priority` a one-line delegate. Nothing in `src/store.rs` or
|
||||
`src/stats.rs` needed correcting beyond a commented-out loop left behind.
|
||||
|
||||
The CLI half of the same drill missed three of its requirements, and every one of them was invisible to
|
||||
those 46 tests:
|
||||
|
||||
- `stats` printed `high / low / medium`, because `main.rs` looped over `[Priority::High, Low, Medium]`.
|
||||
The spec asked for high, medium, low.
|
||||
- `clear` printed nothing, discarding the `usize` that `remove_completed` returns. Expected
|
||||
`cleared 1 completed`.
|
||||
- Untested-because-unreachable, so also unfixed: the `in-progress` arm of `Status::parse`, the `Display`
|
||||
line format, and `Command::parse`'s case folding.
|
||||
|
||||
## Evidence
|
||||
|
||||
`cargo test` in `tasks/`: 17 + 7 + 8 + 14 = 46 passed, 0 failed. Every one of those tests lives in
|
||||
`tests/` and therefore imports the *library*; `run` lives in `src/main.rs`, which a binary crate does not
|
||||
export, so no test in the workspace can call it. The three defects sit entirely inside `run`.
|
||||
|
||||
Measured rather than argued: six one-line mutations planted in a copy of the crate
|
||||
(`lessons/0009-mutants.sh`) and the user's suite run against each. Result before 0009: `0 killed,
|
||||
3 survived, 3 skipped`. The same six against a reference implementation with 13 more tests: `6 killed,
|
||||
0 survived`. The suite's blindness is not a matter of degree — it is a whole surface.
|
||||
|
||||
## Implications
|
||||
|
||||
- **This is the third repeat of LR-0003's finding**, and the first time the cause is structural rather
|
||||
than a missing check. 0003 lost the stderr/exit-1 contract, 0006 lost the `?`/`From` collapse, 0008 lost
|
||||
the CLI output shape. A drill step whose result no test can observe does not land, however clearly the
|
||||
prose states it.
|
||||
- **The fix is architectural, so it is the drill.** 0009 moves `run` into `src/cli.rs` and gives it
|
||||
`out: &mut impl Write`. That is not a lesson about tests bolted onto a refactor; the refactor is the only
|
||||
way the tests can exist, which is exactly the book's argument for a thin `main.rs`.
|
||||
- **Grade a test suite by planted bugs, not by test count.** The user has now run 46 tests and would
|
||||
reasonably infer the crate is well covered. Six mutations refute that in four seconds and give a
|
||||
finishing condition (`6 killed, 0 survived`) that counting cannot.
|
||||
- **Ship no new spec file for 0009.** Every earlier lesson handed over `assert_eq!`s to satisfy; the skill
|
||||
being built here is writing them, so the only deliverable is the mutation script. The drill names the
|
||||
behaviour to pin in prose — per the rule in NOTES line 62 — and leaves the assertions to the user.
|
||||
- Watch for on the next read: whether the assertions are exact (`assert_eq!` on the whole printed string)
|
||||
or hedged (`assert!(out.contains("cleared"))`). The hedged form passes the mutants that matter least and
|
||||
is the likeliest way this drill goes green while staying blind.
|
||||
Reference in New Issue
Block a user