From 6b61d946af5ec15aac81551ff4abe6dc918ef32f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jun 2026 22:08:43 +0200 Subject: [PATCH 1/2] Fix concept-continuity slips in chapter prose Audit chapters for concepts used before introduction and misplaced/dangling cross-references: - strings: retitle 'Your first function' (reader wrote functions in the integers chapter already) - functions: drop dangling reference to a non-existent 'countdown' exercise; introduce recursion fresh - tuples: stop claiming '*' deref is not yet introduced (it was taught in the hashmaps chapter) - structs: point to the moves/borrowing chapters, not the ownership chapter that comes later - password validator: fix '?' in a String-returning snippet (would not compile; matches intro's .unwrap()); drop reference to undefined 'PasswordAdvisor' - smart pointers: Box is introduced in the env-file parser chapter, not the earlier '?' operator chapter --- examples/01_strings_and_chars/2_welcome.md | 2 +- examples/04_functions/4_sum_to.md | 4 ++-- examples/04_functions/4_sum_to.rs | 5 ++--- examples/10_tuples_and_destructuring/1_intro.md | 4 ++-- examples/14_structs_and_methods/6_what_we_learned.md | 2 +- examples/19_password_validator/4_generate.md | 2 +- examples/19_password_validator/5_validate.md | 2 +- examples/23_smart_pointers/6_what_we_learned.md | 4 ++-- 8 files changed, 12 insertions(+), 13 deletions(-) diff --git a/examples/01_strings_and_chars/2_welcome.md b/examples/01_strings_and_chars/2_welcome.md index fd4afcd..985da2b 100644 --- a/examples/01_strings_and_chars/2_welcome.md +++ b/examples/01_strings_and_chars/2_welcome.md @@ -1,4 +1,4 @@ -# Your first function: a welcome message +# A welcome message Time to put `&str` and `String` together. Implement `format_welcome_message` so it returns the string `"Welcome, {name}!"`. diff --git a/examples/04_functions/4_sum_to.md b/examples/04_functions/4_sum_to.md index a28e8ca..a72b92a 100644 --- a/examples/04_functions/4_sum_to.md +++ b/examples/04_functions/4_sum_to.md @@ -1,7 +1,7 @@ # Sum to N -Now do recursion the other way around. -Instead of printing as you go, *return* a value built up from smaller answers. +This exercise is about recursion: a function that calls itself. +Each call *returns* a value built up from the answer to a smaller version of the same problem. Write `sum_to(n)` so it returns `1 + 2 + ... + n`, with `sum_to(0) == 0`. diff --git a/examples/04_functions/4_sum_to.rs b/examples/04_functions/4_sum_to.rs index d00d0ea..6df9325 100644 --- a/examples/04_functions/4_sum_to.rs +++ b/examples/04_functions/4_sum_to.rs @@ -1,9 +1,8 @@ //! Exercise: write a function `sum_to` that returns the sum //! `1 + 2 + ... + n`. By convention, `sum_to(0)` is `0`. //! -//! Solve it recursively. The pattern is the same as `countdown`, -//! but this time each call returns a value that the caller adds to -//! its own work. Don't use a loop, and don't use the closed-form +//! Solve it recursively: each call returns a value that the caller +//! adds to its own work. Don't use a loop, and don't use the closed-form //! `n * (n + 1) / 2`; the point is the recursion. #[test] diff --git a/examples/10_tuples_and_destructuring/1_intro.md b/examples/10_tuples_and_destructuring/1_intro.md index b402046..811091a 100644 --- a/examples/10_tuples_and_destructuring/1_intro.md +++ b/examples/10_tuples_and_destructuring/1_intro.md @@ -29,7 +29,7 @@ fn min_max(values: &[i32]) -> (i32, i32) { let (lo, hi) = min_max(&[3, 1, 4, 1, 5, 9]); ``` -That `min_max` body has three pieces of syntax we haven't formally introduced yet. +That `min_max` body has three pieces of syntax worth a quick note. Don't let them trip you up here: - `values.iter()` walks the slice one element at a time. @@ -37,7 +37,7 @@ Don't let them trip you up here: - `.min()` / `.max()` return an `Option` (they'd return `None` for an empty slice). `.unwrap()` says "I'm sure it's `Some`, give me the value or panic." `Option` is the next chapter. -- The leading `*` *dereferences* the `&i32` that the iterator hands back, so we end up with an owned `i32` instead of a reference. +- The leading `*` *dereferences* the `&i32` the iterator hands back (the same dereference you met in the hashmaps chapter), so we end up with an owned `i32` instead of a reference. When you only care about some fields, use `_` to ignore the rest: diff --git a/examples/14_structs_and_methods/6_what_we_learned.md b/examples/14_structs_and_methods/6_what_we_learned.md index 829ee14..80c0e61 100644 --- a/examples/14_structs_and_methods/6_what_we_learned.md +++ b/examples/14_structs_and_methods/6_what_we_learned.md @@ -9,7 +9,7 @@ You defined a struct, wrote a `new` constructor, added a `&self` method that for - An `impl` block attaches functions to the type. Without `self`, it's an associated function (called as `User::new(..)`); with `self`, it's a method (called as `user.method()`). - The three flavors of `self` say what the method intends to do: `&self` reads, `&mut self` mutates in place, plain `self` consumes. - The same ownership rules from the ownership chapter apply. + The same ownership rules from the moves and borrowing chapters apply. - `Self` (capital S) inside an `impl` block is shorthand for the type. Returning `Self` keeps the constructor signature stable if the type is later renamed. - `format!` is the idiomatic way to build a `String` from a template; same syntax as `println!` but returns the string. diff --git a/examples/19_password_validator/4_generate.md b/examples/19_password_validator/4_generate.md index 8734a45..2f9b2af 100644 --- a/examples/19_password_validator/4_generate.md +++ b/examples/19_password_validator/4_generate.md @@ -2,7 +2,7 @@ Implement `generate_secure_password(length)` that returns a `String` of the requested length, mixing uppercase letters, lowercase letters, digits, and special characters so it would pass a strict validator. -For variability without pulling in `rand`, you can use `std::time::SystemTime::now().duration_since(UNIX_EPOCH)?.subsec_nanos()` as a seed and cycle through your character sets. +For variability without pulling in `rand`, you can use `std::time::SystemTime::now().duration_since(UNIX_EPOCH).unwrap().subsec_nanos()` as a seed and cycle through your character sets. This is **not** cryptographically secure, but it's plenty for this exercise. In real code, use the `rand` crate. diff --git a/examples/19_password_validator/5_validate.md b/examples/19_password_validator/5_validate.md index 87fba88..c417313 100644 --- a/examples/19_password_validator/5_validate.md +++ b/examples/19_password_validator/5_validate.md @@ -22,7 +22,7 @@ Map the final score to `PasswordStrength`: - `>= 70` → `Strong` Push a short message into `feedback` for every rule that *fails*. -That way `PasswordAdvisor` (or your own future code) has something to react to. +That way your own follow-up code has something to react to. The length-related complaint should mention "characters", "length", "short", "longer", or "at least" so the test below can recognise it. ## Useful from the standard library diff --git a/examples/23_smart_pointers/6_what_we_learned.md b/examples/23_smart_pointers/6_what_we_learned.md index 61f99a5..a028633 100644 --- a/examples/23_smart_pointers/6_what_we_learned.md +++ b/examples/23_smart_pointers/6_what_we_learned.md @@ -15,7 +15,7 @@ You boxed an integer and added it back out, defined a recursive expression-tree The compiler can lay it out, and recursion mirrors the data exactly. The same concept underpins parsers, interpreters, and ASTs everywhere. - `Box` is the owned form of a trait object. - It lets you store mixed concrete types behind a single interface (a `Vec>` of pipeline stages, all different structs, driven through one trait) and underpins the `Box` pattern you'll meet in the `?` operator chapter. + It lets you store mixed concrete types behind a single interface (a `Vec>` of pipeline stages, all different structs, driven through one trait) and underpins the `Box` pattern from the env-file parser chapter. - Dynamic dispatch through a trait object costs one vtable lookup per call. That's usually fine. Reach for generics (`fn f`) when you want the compiler to monomorphize away the indirection. @@ -34,4 +34,4 @@ You boxed an integer and added it back out, defined a recursive expression-tree ## Where this goes next The iterators chapter puts iterators front and center, and you'll see how a chain of `.iter().fold(...)` could have replaced the explicit loop in `apply_pipeline`. -The `?` operator chapter brings `Box` and the `?` operator together, which is the day-to-day payoff for understanding `Box` here. +The env-file parser chapter brings `Box` and the `?` operator together, which is the day-to-day payoff for understanding `Box` here. From 4db4fa6afec6a8fedadd88aad58a74c83d28a19c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Jun 2026 22:21:17 +0200 Subject: [PATCH 2/2] Fix slice intro in loops chapter and correct quiz questions - conditionals/loops: introduce the &[i32] slice at point of use in count_evens, before the borrowing and vectors chapters cover slices/references properly - quiz: correct the 'longest' lifetime question (the return is bound to *no longer than* the shorter input, not 'at least as long as' it) - quiz: drop smart-pointer content (RefCell::borrow, Rc/Arc/SharedPtr) so learners who skip the bonus smart-pointers chapter aren't tested on it --- examples/03_conditionals_and_loops/5_count_evens.md | 8 ++++++-- examples/24_rust_fundamentals_quiz/quiz.toml | 10 +++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/examples/03_conditionals_and_loops/5_count_evens.md b/examples/03_conditionals_and_loops/5_count_evens.md index 493f598..9b4b82b 100644 --- a/examples/03_conditionals_and_loops/5_count_evens.md +++ b/examples/03_conditionals_and_loops/5_count_evens.md @@ -1,6 +1,10 @@ # Counting evens with `for` and `continue` -Now you have a slice of integers and want to know how many of them are even. -The natural approach is a `for` loop over the slice with a counter that you bump on every match. +The parameter here is a `&[i32]`, a *slice*: a borrowed view over a sequence of `i32` values that live somewhere else. +Slices, and the `&` that borrows them, get a proper treatment in the borrowing and vectors chapters. +For now the only thing you need is that a `for` loop walks a slice one element at a time, handing you each number in turn. + +You want to count how many of those numbers are even. +A `for` loop over the slice with a counter you bump on every match does the job. This is a good place to use `continue`: skip the odds early and the "do work" branch ends up uncluttered. diff --git a/examples/24_rust_fundamentals_quiz/quiz.toml b/examples/24_rust_fundamentals_quiz/quiz.toml index 3b3f9e6..62a13fc 100644 --- a/examples/24_rust_fundamentals_quiz/quiz.toml +++ b/examples/24_rust_fundamentals_quiz/quiz.toml @@ -42,15 +42,15 @@ answers = [ { text = "`&mut value`", correct = false, explanation = "That's a *mutable* borrow." }, { text = "`&value`", correct = true, explanation = "The `&` operator produces a shared, immutable reference." }, { text = "`*value`", correct = false, explanation = "`*` *dereferences* a reference; it doesn't create one." }, - { text = "`value.borrow()`", correct = false, explanation = "`.borrow()` is a `RefCell` method, not the general way to take a reference." }, + { text = "`value.clone()`", correct = false, explanation = "`.clone()` makes an independent owned copy, not a reference to the original." }, ] [[questions]] prompt = "Which statement about Rust's ownership is correct?" answers = [ - { text = "A value can have multiple owners at the same time.", correct = false, explanation = "Single ownership is the whole point; at most one owner at any moment (use `Rc`/`Arc` only for shared *handles*)." }, + { text = "A value can have multiple owners at the same time.", correct = false, explanation = "Single ownership is the whole point: at most one owner at any moment." }, { text = "When an owner goes out of scope, the value is dropped.", correct = true, explanation = "Right. This is how Rust frees memory deterministically, without a garbage collector." }, - { text = "Ownership can be shared via `SharedPtr`.", correct = false, explanation = "`SharedPtr` is a C++ thing. Rust uses `Rc` (single-threaded) or `Arc` (across threads)." }, + { text = "Assigning a value to a second variable always copies it.", correct = false, explanation = "For non-`Copy` types, assignment *moves* ownership; the original binding can't be used afterwards." }, { text = "Primitive types need manual memory management.", correct = false, explanation = "Primitives implement `Copy`, so they're duplicated on assignment, no manual management." }, ] @@ -131,8 +131,8 @@ fn longest<'a>(x: &'a str, y: &'a str) -> &'a str hint = "`'a` is the *shorter* of `x`'s and `y`'s lifetimes." answers = [ { text = "Both inputs must outlive the function call.", correct = false, explanation = "That's true of every reference parameter; it's not what `'a` adds." }, - { text = "The return value lives strictly *less* than the shorter input.", correct = false, explanation = "Close, but `'a` says \"at *least* as long as\", not \"strictly less than\"." }, - { text = "The return value lives at least as long as the shorter of the two inputs.", correct = true, explanation = "Right. `'a` is the intersection of the two input lifetimes, and the return reference is valid for that whole window." }, + { text = "The return value lives strictly *less* than the shorter input.", correct = false, explanation = "Not quite: the returned reference can be valid for that whole window, so the bound is \"no longer than\" (it may be equal), not \"strictly less than\"." }, + { text = "The return value can't outlive the shorter of the two inputs.", correct = true, explanation = "Right. `'a` is the intersection of the two input lifetimes, so the returned reference is tied to whichever input is dropped first and can't be used past it." }, { text = "The two inputs must have identical lifetimes.", correct = false, explanation = "They don't; the compiler picks `'a` as the shorter of the two and unifies via subtyping." }, ]