diff --git a/examples/00_integers/1_intro.md b/examples/00_integers/1_intro.md index 5ae7020..881d4eb 100644 --- a/examples/00_integers/1_intro.md +++ b/examples/00_integers/1_intro.md @@ -2,8 +2,7 @@ *This chapter was supposed to start with an integer joke. Turns out, it's pointless.* -You already know how numbers work, so let's start with a quick tour of the types -and then spend the rest of the chapter on what Rust does differently. +You already know how numbers work, so we'll take a quick tour of the types and spend our time on what Rust does differently. ```rust let byte: u8 = 255; // a single byte, holds 0 to 255 @@ -14,10 +13,10 @@ let i: usize = 0; // the type for sizes and indices let price: f64 = 19.99; // floating point (f32 is the smaller one) ``` -That covers most of what you'll see in the wild. -Now on to the interesting bits! - -## No silent overflows +Those are the types you'll see most often. +The first difference matters when a value no longer fits its type. + +## No silent overflows If you push a number past its type's maximum, most languages won't tell you. For example, C wraps around, Java wraps around, and Python quietly grows the integer to fit. @@ -36,14 +35,15 @@ let total = hp + bonus; If that bug had been in a C program, the total would be 44 instead of 300. The player would have no idea why their health bar suddenly *dropped* from 200 to 44 after picking up a bonus item. -Rust catches it at the source instead and tells you. -It's best to be explicit about how you want to handle overflow, and Rust gives you three options: +Rust catches the overflow where it happens instead. +When a result may not fit, choose the behavior that matches the problem: - `a.saturating_add(b)` clamps at the maximum, so 255 stays 255. - `a.checked_add(b)` returns `None` on overflow, so you can handle it yourself. - `a.wrapping_add(b)` opts back into wraparound, for the times you actually want it. -(Release builds wrap by default for speed. Reach for these methods to get the same behavior in both debug and release builds.) +Release builds wrap by default for speed. +Reach for these methods when you need the same behavior in both debug and release builds. ## No implicit conversions @@ -61,13 +61,12 @@ let total = price * count as f64; ## Text into numbers -Parsing a string can fail, because the input might not be a number at all. So -`parse` hands back a `Result`. +Parsing a string can fail because the input might not be a number at all, so `parse` hands back a `Result`. ```rust let n: u32 = "123".parse().unwrap_or(0); ``` -We haven't talked about `Result` yet, but the idea is that Rust forces you to deal with the possibility of failure instead of ignoring it. -For now, `.unwrap_or(0)` is fine. -There's a dedicated chapter on `Result` later. +We haven't talked about `Result` yet, but the important part for now is that a failed parse cannot be ignored by accident. +Using `.unwrap_or(0)` is enough for this exercise. +We'll come back to failed parses after introducing `Result`. diff --git a/examples/00_integers/2_hints.md b/examples/00_integers/2_hints.md index 9be384f..90f8f70 100644 --- a/examples/00_integers/2_hints.md +++ b/examples/00_integers/2_hints.md @@ -19,4 +19,3 @@ You'll need a type annotation so it knows which one. 2. `.parse::()` returns a `Result`. The exercise asks for `0` on failure, so reach for `.unwrap_or(0)`. -3. (Forward reference: in the `Result` chapter you'll learn why returning `Result` directly is the better signature.) diff --git a/examples/00_integers/4_damage_with_bonus.md b/examples/00_integers/4_damage_with_bonus.md index 5406124..e22605d 100644 --- a/examples/00_integers/4_damage_with_bonus.md +++ b/examples/00_integers/4_damage_with_bonus.md @@ -4,21 +4,17 @@ Rust never converts between numeric types implicitly. If you want to multiply a `u32` by an `f64`, one of them has to change shape first, and you have to say so. The `as` keyword does the conversion. -The function takes a base damage as `u32` and a bonus as an `f64` percentage (so `50.0` means "+50%"; think a critical hit, an equipment buff, or any other damage modifier), and returns the final damage as a `u32`. -You'll need to: +The function takes base damage as a `u32` and an `f64` bonus percentage, then returns the final damage as a `u32`. +A bonus of `50.0` means adding half of the base damage again, whether it came from a critical hit, equipment, or some other modifier. +Keep the calculation in `f64` so the fractional percentage is not lost, then convert the final damage back to `u32`. -1. Turn the percentage into a multiplier (divide by `100.0`). -2. Multiply it by the base, but only after casting the base to `f64`, because Rust won't mix the two for you. -3. Add the bonus to the base. -4. Cast back to `u32` to return. - -That last cast *truncates* the fractional part toward zero, which is exactly what we want here: most games quantise to whole HP, so `8.085` damage becomes `8`, not `9`. -The final test pins this down. +Converting the final value back to `u32` *truncates* the fractional part toward zero. +That matches games which use whole HP, so `8.085` damage becomes `8`, not `9`. +The final test checks that we truncate rather than round. ## Useful from the standard library - [`as`](https://doc.rust-lang.org/std/keyword.as.html) is the cast operator. - `1.7_f64 as u32` is `1` (truncation), not `2`. - That same truncation is what drops fractional HP in this exercise. -- [`f64::round`](https://doc.rust-lang.org/std/primitive.f64.html#method.round) exists too, and rounds to the nearest integer. - It's the right tool when you want nearest-integer rounding, but this exercise asks for truncation, so you won't need it here. + `1.7_f64 as u32` is `1`, not `2`, because the cast truncates. +- [`f64::round`](https://doc.rust-lang.org/std/primitive.f64.html#method.round) rounds to the nearest integer instead. + This exercise asks for truncation, so compare the two behaviors before choosing. diff --git a/examples/00_integers/5_parse_positive_integer.md b/examples/00_integers/5_parse_positive_integer.md index 8598ae4..3818539 100644 --- a/examples/00_integers/5_parse_positive_integer.md +++ b/examples/00_integers/5_parse_positive_integer.md @@ -1,19 +1,19 @@ # Parsing strings into numbers -`str::parse` is the universal "turn this text into a value of some type" method. -It returns a `Result`, because the input might not be parseable. -We don't have `Result` yet (it gets its own chapter later), so we'll collapse failure to `0` for now. +`str::parse` turns text into the type you ask for. +It returns a `Result` because not every input can become the type you asked for. +We haven't met `Result` yet, so this exercise deliberately maps a failed parse to `0`. -Returning `0` on failure is a *bad idea* in real code: it silently merges "the input was the number zero" with "the input was garbage". -Rust has a much better tool for this in `Option` and `Result`, each of which gets its own chapter. -For now, `parse().unwrap_or(0)` is the shortest way to satisfy the tests. +Returning `0` on failure is a *bad idea* in real code because it makes valid input `"0"` indistinguishable from garbage. +When we get to `Option` and `Result`, we'll preserve that difference. +For now, the tests define `0` as the fallback. Note that `u32` can't be negative, so `"-5".parse::()` will fail and we should also return `0`. If you reach for `i32` first, the test for `"-5"` may surprise you. ## Useful from the standard library -- [`str::parse`](https://doc.rust-lang.org/std/primitive.str.html#method.parse) turns a string into any type that implements `FromStr`. +- [`str::parse`](https://doc.rust-lang.org/std/primitive.str.html#method.parse) turns a string into a type you choose. Returns a `Result` because the input might not be valid. - [`Result::unwrap_or`](https://doc.rust-lang.org/std/result/enum.Result.html#method.unwrap_or) hands back the value on `Ok`, or the fallback you give it on `Err`. Useful for the "just give me a number" path here. diff --git a/examples/00_integers/6_what_we_learned.md b/examples/00_integers/6_what_we_learned.md index e6f68af..a565114 100644 --- a/examples/00_integers/6_what_we_learned.md +++ b/examples/00_integers/6_what_we_learned.md @@ -11,5 +11,5 @@ You met Rust's stance on numbers: overflow is caught rather than ignored, type c Use `as` for a truncating cast, or `.into()` / `.try_into()` when you want a checked conversion. - `as u32` on a float truncates toward zero (`1.7 as u32` is `1`); `f64::round` rounds to the nearest integer. We used truncation for the damage bonus because games drop fractional HP. -- `str::parse()` is the universal text-to-value method. - It returns a `Result`; pair it with `.unwrap_or(...)` until you've met `Result` properly in its own chapter. +- `str::parse()` turns text into a value of a requested type. + It returns a `Result`; for now, pair it with `.unwrap_or(...)` when you need a fallback. diff --git a/examples/01_strings_and_chars/1_intro.md b/examples/01_strings_and_chars/1_intro.md index 9bd3c47..38c0cdc 100644 --- a/examples/01_strings_and_chars/1_intro.md +++ b/examples/01_strings_and_chars/1_intro.md @@ -22,8 +22,8 @@ Before we go further, two words that show up everywhere in Rust: The ownership model is why Rust has two string types in the first place: it tracks who owns each piece of data. The 30-second version: every value has one owner, the value is dropped when that owner goes out of scope, and you can borrow a value without taking it. -The next chapter (moves and `Copy`) and the borrowing chapter make this hands-on, and a later memory chapter ties it together. -For now just keep the mental picture of "one owner, many short-lived borrows." +Next we'll move `String` values and copy integers, then add borrowing after functions and revisit the full memory model later. +For now, keep the mental picture of "one owner, many short-lived borrows." The split between `&str` and `String` is what makes Rust strings both fast and safe. A function that just *reads* text takes `&str`; a function that *produces* new text returns `String`. @@ -48,11 +48,13 @@ For character counts use `s.chars().count()`. UTF-8 means a single visible character can take more than one byte. You'll also meet `.chars()` a lot. -It returns an iterator of `char`, and iterators have many useful adapters like `.next()`, `.count()`, and `.any(...)` (more on iterators in the iterators chapter). +It lets you walk through the `char` values in a string, one at a time. +Rust calls the value it returns an *iterator*, but you do not need the full iterator model yet. +We'll spend more time with methods such as `.next()`, `.count()`, and `.any(...)` later. ## Building a `String` with `format!` -The fastest way to assemble a new `String` is the `format!` macro. +A convenient way to assemble a new `String` is the `format!` macro. It works like `println!`, except instead of printing, it returns the formatted text: ```rust @@ -60,14 +62,12 @@ let name = "Alice"; let greeting: String = format!("Hello, {name}!"); ``` -A few things worth noticing: - -- The `{name}` inside the string is a **captured identifier**. - Rust pulls the variable from the surrounding scope. - (Pre-2021 code uses `format!("Hello, {}!", name)` instead; both still work.) -- The macro returns a `String`, ready to return from your function. -- The exclamation mark (`!`) means it's a macro, not a regular function call. - You'll learn what that distinction buys you later; for now, treat it as a quirky bit of punctuation. +In the format string, `{name}` is a **captured identifier**. +Rust pulls the variable from the surrounding scope. +Pre-2021 code often writes `format!("Hello, {}!", name)` instead, and both forms still work. +The macro returns a `String`, ready to return from your function. +The exclamation mark (`!`) means `format!` is a macro rather than a regular function call. +We'll see what that distinction buys us later, so for now you can treat it as a quirky bit of punctuation. ## A note on `for` loops @@ -79,14 +79,15 @@ for c in "hello".chars() { } ``` -You can read it as "for each `c` produced by the iterator on the right, run the body once." +You can read it as "for each `c` produced on the right, run the body once." The loop variable is a fresh binding scoped to each iteration. -Anything that produces an iterator (a `Vec`, a slice, a `HashMap`, `0..10`, ...) works on the right-hand side. +Ranges, arrays, and collections can also go on the right-hand side because each can produce an iterator. +For now, it is enough to recognize that `.chars()` gives a `for` loop characters to walk through. ## Where to look things up You won't memorize Rust's `std` library, and you don't need to. -Two things you can open in separate tabs right now: +Keep these two reference pages handy: - [`std::fmt`](https://doc.rust-lang.org/std/fmt/) contains everything the formatting macros can do (padding, precision, hex, debug output…). - [`str`](https://doc.rust-lang.org/std/primitive.str.html): the inventory of operations available on any `&str`. diff --git a/examples/01_strings_and_chars/3_count_chars.md b/examples/01_strings_and_chars/3_count_chars.md index 5ba849e..2f40dc4 100644 --- a/examples/01_strings_and_chars/3_count_chars.md +++ b/examples/01_strings_and_chars/3_count_chars.md @@ -1,6 +1,6 @@ # Counting characters -Your first encounter with `&str`. +This is where byte length and character count part ways. In many languages, asking for the "length" of a string gives you back the number of characters. In Rust, `str::len` returns the number of *bytes* in the underlying UTF-8 buffer, which only matches the character count for plain ASCII. diff --git a/examples/01_strings_and_chars/4_shout.md b/examples/01_strings_and_chars/4_shout.md index c3e94d5..e3b0f96 100644 --- a/examples/01_strings_and_chars/4_shout.md +++ b/examples/01_strings_and_chars/4_shout.md @@ -1,8 +1,8 @@ # Borrow in, own out -This step is the canonical "borrowed in, owned out" pattern. -The caller hands you a cheap `&str` view, and you give back a brand new `String` that they get to keep. -You'll see this pattern over and over in real Rust code, so it's worth getting comfortable with the signature now. +Here you borrow text to read it, then return a new `String` that the caller can keep. +The signature captures that handoff: `&str` in, owned `String` out. +You'll see the same shape whenever a function reads existing text to produce different text. ## Useful from the standard library diff --git a/examples/01_strings_and_chars/5_has_uppercase.md b/examples/01_strings_and_chars/5_has_uppercase.md index d73108a..1ec6a22 100644 --- a/examples/01_strings_and_chars/5_has_uppercase.md +++ b/examples/01_strings_and_chars/5_has_uppercase.md @@ -1,7 +1,8 @@ # Iterating over characters -Strings aren't directly indexable in Rust (because UTF-8 characters have varying widths), but you can iterate over their `char`s. -A plain `for c in text.chars()` loop will work, and so will the iterator combinators like `any` or `find`, which usually express "is there at least one ..." checks more directly. +Strings aren't directly indexable in Rust because UTF-8 characters have varying widths, but you can walk through their `char`s. +A plain `for c in text.chars()` loop works. +When the question is "does at least one character match?", `any` expresses that directly and can stop as soon as it finds one. ## Useful from the standard library diff --git a/examples/02_moves_and_copy/1_intro.md b/examples/02_moves_and_copy/1_intro.md index c52ab74..5e8dee9 100644 --- a/examples/02_moves_and_copy/1_intro.md +++ b/examples/02_moves_and_copy/1_intro.md @@ -10,8 +10,8 @@ println!("{s}"); // ERROR: borrow of moved value: `s` ``` Assigning `s` to `t` *moves* the string. -There's now one owner, `t`, and `s` is dead. -Reach for `s` again and the compiler refuses, naming the exact line. +There's now one owner, `t`, and `s` no longer names a value you can use. +Reach for `s` again and the compiler points to the exact move that made it unavailable. In a language with shared mutable pointers this would be a silent bug (two variables aliasing one buffer, one of them freeing it first), and Rust turns it into a compile error. Why move instead of copy? @@ -30,9 +30,9 @@ let b = a; // a is copied, not moved println!("{a} {b}"); // both fine ``` -The rule of thumb: a type that owns nothing on the heap and is cheap to duplicate is `Copy`, and the move rules never bite it. -Everything else moves. +For now, expect small primitive values such as integers and `bool` to be `Copy`, while heap-owning values such as `String` move. +A type's documentation tells you whether it implements `Copy` when the distinction is not obvious. -This is the first chapter where moves matter, because `String` is the first heap-owning type you've met. -Borrowing, using a value without taking it, is the next chapter. -Here we just get comfortable with ownership changing hands. +Moves matter now because `String` is the first heap-owning type you've met. +Here we'll get comfortable with ownership changing hands. +After functions, we'll borrow values so a caller can keep ownership while another function uses them. diff --git a/examples/02_moves_and_copy/2_take_ownership.md b/examples/02_moves_and_copy/2_take_ownership.md index 2a9c2a7..97ea567 100644 --- a/examples/02_moves_and_copy/2_take_ownership.md +++ b/examples/02_moves_and_copy/2_take_ownership.md @@ -2,11 +2,12 @@ When a function parameter has an owned type like `String` (no `&` in front), calling the function *moves* the argument in. The caller's binding is no longer usable afterwards. -The value lives at the callee now, and will be dropped when the callee finishes (unless it hands ownership back via the return value, which is exactly what happens here). +The value now belongs to the called function. +It will be dropped when that function finishes unless the function hands ownership back, which is exactly what the return value does here. -Note the signature: `String` in, `String` out. -Implement the body by mutating the parameter (`s.push_str(...)`) and then returning `s`. -Because you own `s`, you're free to mutate it directly: ownership implies the right to modify. +Read the signature as `String` in, `String` out. +The function can choose to mutate the value it owns, but the parameter binding still needs `mut` before you can change it. +Append the text to `s`, then return the same `String` so the caller owns it again. ## Useful from the standard library diff --git a/examples/02_moves_and_copy/4_what_we_learned.md b/examples/02_moves_and_copy/4_what_we_learned.md index b1d6edb..fa518b3 100644 --- a/examples/02_moves_and_copy/4_what_we_learned.md +++ b/examples/02_moves_and_copy/4_what_we_learned.md @@ -10,4 +10,4 @@ You moved a `String` into a function and back out, and saw that an `i32` copies Rust makes deep copies explicit through `.clone()`. - `Copy` types (integers, `bool`, `char`, fixed-size arrays of those) duplicate bit-for-bit instead of moving, so the original stays usable. - The owner is responsible for the value: when it goes out of scope, the value is dropped, with no garbage collector involved. -- Borrowing, coming up next, lets you hand a value to a function without giving up ownership at all. +- When we get to borrowing, you'll hand a value to a function without giving up ownership at all. diff --git a/examples/03_conditionals_and_loops/5_count_evens.md b/examples/03_conditionals_and_loops/5_count_evens.md index 4d8b8a9..582d803 100644 --- a/examples/03_conditionals_and_loops/5_count_evens.md +++ b/examples/03_conditionals_and_loops/5_count_evens.md @@ -4,8 +4,8 @@ The parameter here is a `&[i32]`, a *slice*: a borrowed view over a sequence of We'll spend more time with slices, and the `&` that borrows them, 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. +A `for` loop gives you each number in turn. +Keep a counter and bump it whenever the number is even. When the current number is odd, you can use `continue` to skip straight to the next one. You can then increment the counter without nesting that line inside another `if`. diff --git a/examples/04_functions/1_intro.md b/examples/04_functions/1_intro.md index 50051a7..e272f54 100644 --- a/examples/04_functions/1_intro.md +++ b/examples/04_functions/1_intro.md @@ -2,7 +2,7 @@ You've been inside a function since the first line you wrote. `fn main()` is one, and every `println!(...)` is a call (the `!` marks it as a macro). -So instead of explaining what a function is, this chapter covers the parts of `fn` that Rust does its own way: explicit parameter and return types, blocks as expressions, and the trailing-semicolon rule that decides what gets returned. +So instead of explaining what a function is, we'll focus on the parts Rust does its own way: explicit parameter and return types, blocks as expressions, and the trailing-semicolon rule that decides what gets returned. ## Anatomy @@ -24,8 +24,9 @@ let sum = add(2, 3); // sum: i32 = 5 ## Expressions, not statements -The body of a function is a *block*, and a block is one or more statements followed by an optional final expression. -The final expression (if there is one, and it has no trailing semicolon) becomes the value of the block, which becomes the return value of the function. +The body of a function is a *block*: zero or more statements followed by an optional final expression. +When that final expression has no trailing semicolon, its value becomes the value of the block. +For a function body, that block value is also the function's return value. ```rust fn double(n: i32) -> i32 { @@ -45,9 +46,7 @@ That semicolon thing trips up newcomers. The rule is short: a semicolon turns an expression into a statement (which has no value). Forgetting one at the end of the function is the *correct* thing to do when you want the value to be returned. Adding one accidentally turns the body into "do this, then return `()`" and the compiler will complain that the types don't match. -The first exercise lets you feel that error first-hand. - -The two exercises after it each pull at one more thread: returning a value built recursively, and modifying a parameter inside the body. +In the first exercise, you'll make that error disappear by changing a single character. ## A few good habits @@ -55,14 +54,11 @@ The two exercises after it each pull at one more thread: returning a value built Single-purpose functions are easier to test and easier to read. - Use parameter names that say what the value *is*, not what type it is: `width: u32`, not `w: u32`. - Prefer the *least demanding* parameter type that still lets you do the job. - "Least demanding" means: ask the caller for as little as possible. + In other words, ask the caller for as little as possible. If you only need to *read* a string, take `&str`, not `String`. - Three reasons this matters: - 1. Taking `String` would force the caller to hand over their value (or `.clone()` it). - Taking `&str` lets them keep it. - 2. A `&str` parameter accepts string literals (`"hi"`), borrows of owned strings (`&my_string` coerces from `&String` to `&str`), and slices of larger buffers, all without conversion at the call site. - 3. A `&str` is just a pointer and a length; passing one costs nothing. - A `String` parameter would mean moving (or cloning) a heap buffer on every call. + Taking `String` would force the caller to hand over the value or clone it, while `&str` lets them keep it. + A `&str` parameter also accepts string literals, borrowed `String` values, and slices of larger text buffers without conversion at the call site. + Borrowing does not clone the string, and moving a `String` would transfer ownership without moving its heap allocation. - The same idea extends to other types: take `&[T]` instead of `&Vec`, `&Path` instead of `&PathBuf`, and so on. - We'll come back to this pattern in the vectors and ownership chapters. + The same idea extends to other types, such as `&[T]` instead of `&Vec` and `&Path` instead of `&PathBuf`. + We'll use this pattern again with vectors and when we revisit ownership. diff --git a/examples/04_functions/3_stray_semicolon.md b/examples/04_functions/3_stray_semicolon.md index 29dee0f..5106470 100644 --- a/examples/04_functions/3_stray_semicolon.md +++ b/examples/04_functions/3_stray_semicolon.md @@ -1,9 +1,9 @@ # A stray semicolon -This function takes an `i32`, declares an `i32` return type, multiplies by two. -Yet, the compiler refuses to compile. +This function takes an `i32`, promises to return an `i32`, and multiplies the input by two. +Still, the compiler refuses to compile it. -Run the tests, read the error, and fix it. +Run the tests and read the error before you change anything. -The lesson hiding behind that error is the difference between an *expression* (which has a value) and a *statement* (which doesn't). -One character decides which a line is, and that character decides what your function returns. +The error points to the difference between an *expression*, which has a value, and a *statement*, which doesn't. +One character decides which one the final line is, and therefore what the function returns. diff --git a/examples/04_functions/5_cap_at.md b/examples/04_functions/5_cap_at.md index 1455f57..f77eecf 100644 --- a/examples/04_functions/5_cap_at.md +++ b/examples/04_functions/5_cap_at.md @@ -5,12 +5,11 @@ Both arguments are `i32`. The logic is one `if` away. Write the function the most natural way you can think of. -The most natural way doesn't compile, because function parameters are immutable bindings by default (like `let`), so reassigning `value` is rejected. -The fix is one keyword in one place. -Read the error, it points right at it. +Your first version may not compile, and that failure is part of the exercise. +Read the error before changing anything because it tells you why the assignment is rejected. Once it compiles, look at the second test. The caller's variable is untouched even though the function reassigned its parameter. That's because `i32` is `Copy`, so the function received its own copy to mutate. -The moves chapter showed the other half of this: a non-`Copy` type like `String` gets moved in instead of copied. -The next chapter, borrowing, shows how to lend a value to a function without giving it up at all. +You saw the other half with moves: a non-`Copy` type such as `String` is moved in instead of copied. +Next we'll borrow a value so a function can use it without taking ownership. diff --git a/examples/04_functions/7_what_we_learned.md b/examples/04_functions/7_what_we_learned.md index d4833d8..9c77553 100644 --- a/examples/04_functions/7_what_we_learned.md +++ b/examples/04_functions/7_what_we_learned.md @@ -1,11 +1,11 @@ # Wrapping up functions -Three small exercises, three new ideas: +Across these exercises, you used three parts of Rust's function model: - **`stray_semicolon`** drove home the difference between an expression and a statement: one trailing `;` is the line between "return this value" and "return `()`". - **`sum_to`** built a value with recursion, where each call's answer feeds into the caller's answer. - **`cap_at`** showed that function parameters are immutable bindings by default, and that adding `mut` to the parameter name affects only the function's own copy. The caller's variable is untouched because `i32` is `Copy`. -The thread running through all of this: the body of a function is a block whose final expression (no trailing semicolon) is the return value. -Everything else in the chapter is a consequence of that one rule. +The central rule is that a function body is a block whose final expression, without a trailing semicolon, becomes the return value. +Parameters are bindings too, so they are immutable unless you add `mut` inside the function. diff --git a/examples/05_borrowing_and_references/2_borrow_string.md b/examples/05_borrowing_and_references/2_borrow_string.md index 6fd6fed..2b35cef 100644 --- a/examples/05_borrowing_and_references/2_borrow_string.md +++ b/examples/05_borrowing_and_references/2_borrow_string.md @@ -5,16 +5,17 @@ That's a shared borrow, written `&T` in the signature. The caller keeps ownership; the callee gets temporary read-only access. This function takes `&str` rather than `&String`. -`&str` is the universal "borrowed string slice" type: a string literal is already a `&'static str`, and `&String` automatically coerces to `&str`, so `&str` parameters accept both without forcing the caller to convert. -Reach for `&str` by default when you're just reading. +A string literal is already a `&str`, and a borrowed `String` automatically becomes one at the call site. +That means the same parameter accepts both without making the caller convert anything. +Reach for `&str` by default when you only need to read text. -The body is a one-liner: call `.len()` on the slice. -The point of the exercise is the signature: notice that after the call, the caller's `s` is still usable in the test below. +The body only needs to call `.len()` on the slice. +Pay more attention to the signature and the test: after the call, the caller can still use `s` because the function only borrowed it. ## Useful from the standard library - [`str::len`](https://doc.rust-lang.org/std/primitive.str.html#method.len) is the byte length of the slice. The chapter on strings covers why that's not the same as a character count. -- The "deref coercion" from `&String` to `&str` is what lets the test pass `&s` directly. - No `.as_str()` needed. +- Rust calls the automatic conversion from `&String` to `&str` a "deref coercion." + It is why the test can pass `&s` directly, with no `.as_str()` call. diff --git a/examples/05_borrowing_and_references/3_mutate_string.md b/examples/05_borrowing_and_references/3_mutate_string.md index 157abd3..8c3f1d5 100644 --- a/examples/05_borrowing_and_references/3_mutate_string.md +++ b/examples/05_borrowing_and_references/3_mutate_string.md @@ -4,12 +4,11 @@ Sometimes you want to modify a value in place without taking ownership of it. That's a mutable borrow: `&mut T`. The caller still owns the value, but the callee gets exclusive write access for the duration of the call. -Two things to notice in the signature: +The parameter is `&mut String`, not `&mut str`. +Appending text may require the owned `String` to grow its buffer, while a string slice has a fixed length. -1. The parameter is `&mut String`, not `&mut str`. - We need the *owned* `String` because growing it (with `push_str`) may reallocate; a bare string slice has a fixed length. -2. There's no return value. - The mutation happens through the reference and is visible to the caller after the call returns. +The function does not need to return the `String`. +It changes the value through the reference, and the caller sees that change after the call returns. On the call site (see the test): the caller has to write `&mut s` explicitly, and `s` itself has to have been declared `let mut s = ...`. Mutability is opt-in at every layer. diff --git a/examples/05_borrowing_and_references/4_experiments.md b/examples/05_borrowing_and_references/4_experiments.md index d2ce19f..b730dca 100644 --- a/examples/05_borrowing_and_references/4_experiments.md +++ b/examples/05_borrowing_and_references/4_experiments.md @@ -1,7 +1,8 @@ # Experiments: get the errors on purpose -Passing the previous tests is the easy part of this chapter. -Ownership only really clicks once you've seen the canonical errors with your own eyes, so the messages feel familiar later (the iterators chapter, the `?` operator chapter, ...) instead of like a brick wall. +Passing the previous tests was the easy part. +The harder skill is learning to read ownership errors until they feel familiar instead of like a brick wall. + Each test below is paired with a commented-out line. Uncomment one at a time, run the tests, read the error carefully, then comment it out again before moving on. @@ -15,7 +16,7 @@ The three errors you'll trigger correspond to the three rules of the borrow chec 3. You can't have a mutable reference while a shared reference is still in use. Re-read each compiler message until you can explain in one sentence *why* the compiler is complaining. -That's the muscle this chapter is building. +Once you can do that, you can change the code for a reason instead of guessing. ## Useful from the standard library diff --git a/examples/05_borrowing_and_references/5_what_we_learned.md b/examples/05_borrowing_and_references/5_what_we_learned.md index 7fc4d84..776328b 100644 --- a/examples/05_borrowing_and_references/5_what_we_learned.md +++ b/examples/05_borrowing_and_references/5_what_we_learned.md @@ -11,5 +11,5 @@ You borrowed a `String` read-only as `&str`, mutated one through `&mut String`, - Mutability is opt-in at every layer: the binding (`let mut x`), the parameter (`&mut T`), and the call site (`&mut x`). - Default to `&str` over `&String` (and `&[T]` over `&Vec`) for read-only parameters. Slice types accept more callers thanks to deref coercion. -- The compiler errors are the lesson. - Once you can say in one sentence why the compiler is complaining, you've built the muscle this chapter is for. +- The compiler errors are part of the lesson. + Once you can explain one in a sentence, you can decide what ownership or borrow needs to change instead of guessing. diff --git a/examples/06_word_count/1_intro.md b/examples/06_word_count/1_intro.md index 720c2f8..9100e6b 100644 --- a/examples/06_word_count/1_intro.md +++ b/examples/06_word_count/1_intro.md @@ -3,8 +3,8 @@ Congratulations, you've covered enough of Rust to write a small, useful program without any extra ceremony! Time for a short break to enjoy the view. -The next three steps build a tiny word-count library. -The whole chapter is just strings (the strings chapter), `for` loops (the conditionals and loops chapter), and functions (the functions chapter) applied together. +Over the next three steps, we'll build a tiny word-count library. +We'll combine the strings, `for` loops, and functions you already know rather than introduce another language feature. This first version is the running example we'll keep refactoring throughout the course. @@ -20,7 +20,8 @@ for word in "hello world\nrust".split_whitespace() { ``` It splits on any run of whitespace (spaces, tabs, newlines) and skips empties, which is what you want for natural text. -Don't worry yet about what kind of thing `.split_whitespace()` returns (it's an *iterator*, which we will cover later). +Don't worry yet about the exact type returned by `.split_whitespace()`. +It is an *iterator*, and we'll unpack that term later. ## Walking characters @@ -32,4 +33,4 @@ for c in "hi".chars() { } ``` -Both `.split_whitespace()` and `.chars()` are exactly the tools we need to count words and characters in a string. +With `.split_whitespace()` for words and `.chars()` for characters, a `for` loop can count either one. diff --git a/examples/06_word_count/2_word_count.md b/examples/06_word_count/2_word_count.md index b908e40..d566347 100644 --- a/examples/06_word_count/2_word_count.md +++ b/examples/06_word_count/2_word_count.md @@ -1,9 +1,10 @@ # Counting words -Your first function: given a string of text, return how many words it contains. -Words are anything separated by whitespace, so `"hello world"` has two words and `" "` has zero. +Start with the smallest piece of the library: given some text, return how many words it contains. +For this exercise, words are anything separated by whitespace, so `"hello world"` has two and `" "` has none. -The recipe is the simplest possible: keep a counter, walk the text with `for ... in text.split_whitespace()`, bump the counter on each iteration, return it at the end. +Keep the implementation deliberately manual. +Walk the pieces from `text.split_whitespace()`, bump a counter for each one, and return the counter when the loop ends. ## Useful from the standard library diff --git a/examples/06_word_count/4_longest_word.md b/examples/06_word_count/4_longest_word.md index ffb7bb4..751f356 100644 --- a/examples/06_word_count/4_longest_word.md +++ b/examples/06_word_count/4_longest_word.md @@ -17,8 +17,9 @@ for x in candidates { } ``` -This is the manual version of "max by some property". -The iterators chapter shows it as a one-liner; doing it once by hand makes the shortcut feel like a reward rather than magic. +This is the manual version of "max by some property." +When we get to iterators, we'll write the same search as a one-liner. +Doing it once by hand first makes each part of that shortcut recognizable. ## Useful from the standard library diff --git a/examples/06_word_count/5_what_we_learned.md b/examples/06_word_count/5_what_we_learned.md index 74c1ec4..9f03931 100644 --- a/examples/06_word_count/5_what_we_learned.md +++ b/examples/06_word_count/5_what_we_learned.md @@ -15,7 +15,7 @@ That's enough to build a real, useful tool, and it's the same shape you'll keep ## What comes next -You'll meet `split_whitespace` and `chars` again in **the iterators chapter**, where the three loops you just wrote collapse to: +When we get to **iterators**, we'll collapse the three loops you just wrote to: ```rust fn word_count(text: &str) -> usize { text.split_whitespace().count() } @@ -23,4 +23,4 @@ fn char_count(text: &str) -> usize { text.chars().count() } fn longest_word(text: &str) -> usize { text.split_whitespace().map(|w| w.chars().count()).max().unwrap_or(0) } ``` -Then in **the word frequencies chapter** we'll go further and ask not just *how many* words a text contains, but *which* words appear and *how often* each one shows up. +Later, we'll extend this example from counting all words to recording which words appear and how often. diff --git a/examples/07_enums_and_pattern_matching/1_intro.md b/examples/07_enums_and_pattern_matching/1_intro.md index c8f0b83..9d9cd74 100644 --- a/examples/07_enums_and_pattern_matching/1_intro.md +++ b/examples/07_enums_and_pattern_matching/1_intro.md @@ -9,7 +9,7 @@ Dad is right, enums are the best. If you know the crippled form of enums in other languages (*cough* C), I'm so sorry for you. In Rust, they are a pure delight to work with. -But first things first: an `enum` is a type whose value is one of a fixed set of variants. +An `enum` is a type whose value is one of a fixed set of variants. Think of it as a "this or that or that" type. ```rust @@ -60,9 +60,10 @@ enum HttpStatus { The `#[...]` syntax is an *attribute*: extra instructions for the compiler attached to the item below. `derive` is the most common one. It says "please write the boilerplate for these capabilities for me." -Each name inside the parentheses is a *trait* (Rust's name for a shared interface, similar to a Java interface or a Haskell type class; traits get their own chapter later). +Each name inside the parentheses is a *trait*, Rust's name for a shared interface, similar to a Java interface or a Haskell type class. +We'll spend more time with traits later. -The two we use right away: +For now, you need two: - **`Debug`** lets you print the value with the `{:?}` formatter, so `println!("{status:?}")` prints `NotFound` instead of refusing to compile. Useful in `dbg!`, `assert_eq!` failure messages, and quick log lines. @@ -70,8 +71,7 @@ The two we use right away: Without it, comparing two `HttpStatus` values is a compile error; with it, `status == HttpStatus::Ok` just works, and `assert_eq!` in tests can compare whole enum values. Derive works on enums and structs whose fields all implement the same traits. -The compiler writes the obvious implementation. -For `PartialEq` on an enum, that means "two values are equal if they're the same variant with equal payloads." +For `PartialEq` on an enum, the generated implementation considers two values equal when they have the same variant and equal payloads. You can always write the implementation by hand instead when you need different behaviour. diff --git a/examples/07_enums_and_pattern_matching/2_status_code.md b/examples/07_enums_and_pattern_matching/2_status_code.md index 1dccf17..7eaf94d 100644 --- a/examples/07_enums_and_pattern_matching/2_status_code.md +++ b/examples/07_enums_and_pattern_matching/2_status_code.md @@ -1,7 +1,7 @@ # Mapping variants to values -Your first `match`: turn each `HttpStatus` variant into the numeric code it represents. -The compiler will complain if you forget a variant, which is exactly what you want. +Write your first `match` by turning each `HttpStatus` variant into the numeric code it represents. +If you forget one, the compiler points to the incomplete `match` before the program can run. ## Useful from the standard library diff --git a/examples/07_enums_and_pattern_matching/4_what_we_learned.md b/examples/07_enums_and_pattern_matching/4_what_we_learned.md index b880eec..aa0383f 100644 --- a/examples/07_enums_and_pattern_matching/4_what_we_learned.md +++ b/examples/07_enums_and_pattern_matching/4_what_we_learned.md @@ -11,6 +11,6 @@ You defined an enum with a fixed set of variants, mapped each variant to a value - `match` is exhaustive: leave a variant unhandled and the compiler refuses to build. Add a new variant later and every `match` that needs updating tells you exactly where. - `|` lets multiple patterns share an arm (`200 | 201 | 204 => ...`), and `_` is the catch-all when you want to ignore the rest. -- `#[derive(Debug, PartialEq)]` is the usual pair on a plain enum: one for `{:?}` printing, one for `==`. +- Derive `Debug` when you want `{:?}` printing and `PartialEq` when you want `==` comparisons. Add `Clone, Copy` when the variants carry no heap data so values can be passed around freely. - For a single-variant check, `matches!(value, Variant)` is the compact form; `value == Variant` works equally well when `PartialEq` is derived. diff --git a/examples/08_vectors/1_intro.md b/examples/08_vectors/1_intro.md index c264397..68b9e06 100644 --- a/examples/08_vectors/1_intro.md +++ b/examples/08_vectors/1_intro.md @@ -1,6 +1,6 @@ # Vectors -If you stare at a problem for long enough, it starts turn into a vector. +If you stare at a problem for long enough, it starts to turn into a vector. `Vec` is the workhorse of Rust's collection types. ## Arrays first: where vectors come from @@ -12,13 +12,13 @@ An array `[T; N]` is a fixed-size, contiguous chunk of values whose length is pa let bytes: [u8; 4] = [10, 20, 30, 40]; // exactly four u8s, forever ``` -Because the length is known at compile time, the whole array lives **on the stack**, the same place your local variables and function parameters live. -Stack storage is essentially free: allocation is "move the stack pointer by `4 * size_of::()` bytes," and cleanup happens automatically when the function returns. +Because this array is a local variable with a compile-time length, its elements can live directly **on the stack** alongside the function's other local data. +Setting aside that stack space is cheap, and Rust reclaims it automatically when the function returns. The catch is that you can't grow it. `bytes.push(50)` doesn't compile, because there's nowhere to grow *into*: the next bytes on the stack already belong to somebody else. `Vec` solves that by storing the elements **on the heap** instead. -A `Vec` value is a tiny header on the stack (pointer + length + capacity) that points at a buffer the allocator hands you. +The local `Vec` value is a small header containing a pointer, a length, and a capacity, while the allocator provides the buffer it points to. When you `push` and the buffer fills up, `Vec` asks for a bigger one and copies the elements over. The header stays the same size; the buffer behind it grows. @@ -30,9 +30,9 @@ A quick mental model: | `Vec` | Heap | Run time | Yes | | `&[T]` | Wherever the owner put it (just a pointer + length) | n/a | n/a | -This distinction is one of the things Rust makes you confront that many languages hide. -In Python or Java, *every* list is heap-backed and you don't get a choice; in C you'd reach for either a fixed-size array or `malloc` by hand. -Rust gives you both, with the same ownership rules applied to either. +If you're coming from Python or Java, `Vec` is the closer match for the lists you use every day. +In C, the same choice is closer to picking a fixed-size array or managing an allocation yourself. +Rust gives you both choices, and its ownership rules apply to either one. ## Vectors: growable, heap-allocated @@ -40,14 +40,14 @@ Rust gives you both, with the same ownership rules applied to either. The `` is a generic parameter: it works with any type, but a single `Vec` only holds one type at a time. So `Vec` is a vector of 32-bit integers, `Vec` is a vector of owned strings. -Two ways to create one: +You can start with an empty vector or with its initial items: ```rust let mut empty: Vec = Vec::new(); -let with_items = vec![1, 2, 3]; // the vec! macro is the usual way +let with_items = vec![1, 2, 3]; // vec! starts with these three items ``` -Most operations need a mutable reference. Note the `&mut`: +Changing a vector requires mutable access, while reading it only needs a shared borrow: ```rust let mut list = vec!["bread"]; @@ -55,10 +55,10 @@ list.push("milk"); // requires `mut` let count = list.len(); // borrow without mut ``` -A few rules of thumb that will save you trouble: +When you choose a parameter type, start from what the function needs to do: - Take a slice (`&[T]`) as input when the function only needs to *read* the data. - This is the vector chapter's version of the `&str` rule from the functions chapter: `&[i32]` accepts a borrow of a `Vec` (`&my_vec` coerces to `&[i32]`), a borrow of an array (`&[1, 2, 3]`), or a sub-slice of either, all without conversion. + This is the same idea as the `&str` rule you met with functions: `&[i32]` accepts a borrow of a `Vec` (`&my_vec` coerces to `&[i32]`), a borrow of an array (`&[1, 2, 3]`), or a sub-slice of either, all without conversion. A parameter typed `&Vec` would only accept the first one and would offer nothing in return. - Take `&mut Vec` when you need to add or remove items. - Take `Vec` (no reference) when you actually want to consume the vector and take ownership. diff --git a/examples/08_vectors/2_add_item.md b/examples/08_vectors/2_add_item.md index 2df3dd2..f991e9b 100644 --- a/examples/08_vectors/2_add_item.md +++ b/examples/08_vectors/2_add_item.md @@ -1,8 +1,8 @@ # Adding items A `Vec` isn't frozen once you build it. -This step changes the list in place: it pushes a new item onto the end. -The `&mut Vec` says "I need exclusive access for a moment", and that exclusive borrow is what lets us push. +Here you change the list in place by pushing a new item onto the end. +The `&mut Vec` says "I need exclusive access for a moment," and that exclusive borrow is what lets you push. ## Useful from the standard library diff --git a/examples/08_vectors/4_create_shopping_list.md b/examples/08_vectors/4_create_shopping_list.md index eb3c467..023d666 100644 --- a/examples/08_vectors/4_create_shopping_list.md +++ b/examples/08_vectors/4_create_shopping_list.md @@ -1,8 +1,7 @@ # Building a list from borrowed slices -The trickiest of the three: each input is a `&str`, but the output is a `Vec`. -Each borrowed slice has to become an owned `String` somewhere along the way. -The `String::from` / `.to_string()` / `.to_owned()` family all do this conversion. +This time the input and output hold different string types: each input is a borrowed `&str`, while the output must own its `String`s. +That means every item needs to become an owned `String` before it can live in the result. ## Useful from the standard library diff --git a/examples/08_vectors/5_what_we_learned.md b/examples/08_vectors/5_what_we_learned.md index 0dea5e2..2737adb 100644 --- a/examples/08_vectors/5_what_we_learned.md +++ b/examples/08_vectors/5_what_we_learned.md @@ -7,7 +7,7 @@ You worked through every form a `Vec` parameter can take: a shared borrow for re - `Vec` is a growable, heap-allocated array. The `` is generic, but a single `Vec` only holds one type at a time. - Build them with `Vec::new()` for an empty one, or the `vec![...]` macro when you already have the contents. -- The parameter version says what you intend to do: `&[T]` or `&Vec` to read, `&mut Vec` to add or remove, plain `Vec` to consume the whole thing. +- Choose the parameter from the operation: `&[T]` to read, `&mut Vec` to add or remove, and plain `Vec` to consume the whole vector. - `push` appends, `pop` removes the last item and returns `Option`, `len` and `is_empty` answer the obvious questions. - Index access (`list[i]`) panics on out-of-bounds; `list.get(i)` returns `Option<&T>` and is the safer default. - A `for item in &list` loop yields `&T`. diff --git a/examples/09_hashmaps/1_intro.md b/examples/09_hashmaps/1_intro.md index 8a4db6d..786ed57 100644 --- a/examples/09_hashmaps/1_intro.md +++ b/examples/09_hashmaps/1_intro.md @@ -3,7 +3,7 @@ A `HashMap` stores key-value pairs and lets you look up a value by its key in (on average) constant time. Nobody ever got fired for using it for caches, indexes, counters, configuration, and anything else where "given X, find Y" is the question. -Contrary to `Vec`, `HashMap` is not in scope by default, so you have to import it first: +Unlike `Vec`, `HashMap` is not in scope by default, so you have to import it first: ```rust use std::collections::HashMap; @@ -14,13 +14,11 @@ config.insert("host".to_string(), "localhost".to_string()); let host = config.get("host"); // Option<&String> ``` -Two things to notice: +The type annotation says that every key in this map is a `String`, and so is every value. +If you need several possible value types in one map, an enum can represent those choices. -- Keys and values can be any type, but **all keys share one type and all values share one type**. - Mix-and-match goes through enums. -- `.get(key)` returns `Option<&V>`, not `V`. - Missing keys are explicit, no null. - Use `.unwrap_or(...)` or pattern matching to handle absence. +The `.get(key)` call returns `Option<&V>`, not `V`. +A missing key becomes `None` instead of a null value, so you handle the absence with `.unwrap_or(...)` or pattern matching. A common pattern is "increment a counter for this key, default to 0": @@ -45,5 +43,6 @@ let r: &mut i32 = &mut n; ``` Without the `*`, you'd be trying to add `1` to a reference, which the compiler won't let you do. -References were introduced back in the borrowing chapter; for now it's enough to know that when a function returns `&mut T`, you reach the `T` through `*`. +You met references in the borrowing chapter. +Here, the practical rule is that when a function returns `&mut T`, you reach the `T` through `*`. diff --git a/examples/09_hashmaps/5_count_words.md b/examples/09_hashmaps/5_count_words.md index 447411e..07d2e69 100644 --- a/examples/09_hashmaps/5_count_words.md +++ b/examples/09_hashmaps/5_count_words.md @@ -1,8 +1,8 @@ # Counting with `entry` -Counting occurrences is the canonical "look up; if missing, insert a default; then update" workflow. -Doing it by hand with `contains_key` and `get_mut` works but does two lookups and fights the borrow checker. -The `entry` API does it in one step. +A word counter needs to look up each word, start missing counts at zero, and then increment them. +Doing that by hand with `contains_key` and `get_mut` works, but it takes two lookups. +The `entry` API combines the lookup and default insertion. ## Useful from the standard library diff --git a/examples/09_hashmaps/6_what_we_learned.md b/examples/09_hashmaps/6_what_we_learned.md index 001ee21..2524aa4 100644 --- a/examples/09_hashmaps/6_what_we_learned.md +++ b/examples/09_hashmaps/6_what_we_learned.md @@ -14,4 +14,4 @@ You built a configuration map from scratch, updated and read values, and used th - `entry(key).or_insert(default)` is the idiomatic "look up; if missing, insert a default; then return a `&mut V`" pattern. It does one lookup instead of two and sidesteps the borrow checker. - Reach through a `&mut T` with `*` to update the value it points at: `*map.entry(k).or_insert(0) += 1`. - References get a proper treatment in the borrowing chapter. + This is the same dereference operation you met in the borrowing chapter. diff --git a/examples/10_tuples_and_destructuring/1_intro.md b/examples/10_tuples_and_destructuring/1_intro.md index 811091a..4ea9248 100644 --- a/examples/10_tuples_and_destructuring/1_intro.md +++ b/examples/10_tuples_and_destructuring/1_intro.md @@ -29,14 +29,14 @@ 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 worth a quick note. -Don't let them trip you up here: +You only need a rough reading of the `min_max` body for now. +We'll spend more time with iterators and `Option` soon, but these details are enough to follow the example: - `values.iter()` walks the slice one element at a time. - Iterators get a full chapter later; for now read it as "give me each element in turn." + For now, read it as "give me each element in turn." - `.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. + We'll look at `Option` next. - 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: @@ -46,5 +46,6 @@ let (first, _) = ("Alice", "Smith"); ``` Tuples are great for short-lived "two or three values that belong together" situations. -When the tuple grows or you find yourself passing it around a lot, that's a hint to define a `struct` instead (the structs chapter). +When the tuple grows or you keep passing the same shape around, give those fields names with a `struct` instead. +We'll work with structs after `Result`. diff --git a/examples/10_tuples_and_destructuring/3_rectangle_measurements.md b/examples/10_tuples_and_destructuring/3_rectangle_measurements.md index 232ba71..9f3d28f 100644 --- a/examples/10_tuples_and_destructuring/3_rectangle_measurements.md +++ b/examples/10_tuples_and_destructuring/3_rectangle_measurements.md @@ -6,7 +6,7 @@ The caller destructures the result into named bindings. ## Useful from the standard library - The arithmetic operators `*` and `+` are all you need here. - Both `u32` results fit easily for any sane rectangle. + The dimensions in the tests keep both `u32` results within range. - Tuple construction is just parentheses: `(area, perimeter)`. The return type `(u32, u32)` already tells the compiler what shape to expect. - The caller in the test uses `let (area, perimeter) = ...` to destructure the return into named bindings, the mirror image of how you build it. diff --git a/examples/10_tuples_and_destructuring/4_get_first_name.md b/examples/10_tuples_and_destructuring/4_get_first_name.md index 82a2600..58d1330 100644 --- a/examples/10_tuples_and_destructuring/4_get_first_name.md +++ b/examples/10_tuples_and_destructuring/4_get_first_name.md @@ -3,18 +3,15 @@ You can destructure a tuple right in the function parameter list, or inside the body with a `let` binding. Either way, you pull out the pieces by position. -Watch out for ownership: a tuple of `String`s is *moved* into the function, while a tuple of integers is *copied*. -"Moved" means the caller's binding is no longer usable afterwards, because the value's single owner is now the function parameter rather than the caller. -"Copied" means the value is duplicated bit-for-bit, so the caller keeps theirs and the function gets its own. -The split is decided by a trait called `Copy`: types that are tiny and have no heap data (integers, bools, `char`, fixed-size arrays of those, and tuples made entirely of `Copy` types) implement it; types that own heap data (like `String` or `Vec`) deliberately don't. -The doc-comment below has more on this, and the moves chapter covered move semantics in depth. +Ownership still applies when you destructure. +Passing a tuple of `String`s by value moves the whole tuple into the function, so the caller cannot use it afterward. +A tuple of integers is `Copy`, which gives the function its own copy and leaves the caller's value usable. +This is the same move-versus-copy distinction you worked through in the moves chapter. ## Useful from the standard library - [Rust by Example: destructuring tuples](https://doc.rust-lang.org/rust-by-example/flow_control/match/destructuring/destructure_tuple.html) shows the `let (a, b) = pair;` form and how `_` can ignore parts you don't want to bind. - Field-by-index access (`full_name.0`) also works, but a destructure with a meaningful name like `first` reads better at the call site. -- Anything that isn't `Copy` (like `String`) is *moved* when bound by destructuring, so the caller's binding becomes unusable: ownership of the underlying heap buffer transferred into the function. - `Copy` types (integers, bools, `char`, and tuples of those) are duplicated instead, so the caller keeps their copy. - After this function returns, the caller's `(String, String)` tuple is gone. - The moves chapter covered this in depth. +- Anything that isn't `Copy`, such as `String`, moves when destructured by value. + A tuple is only `Copy` when all of its elements are `Copy`. diff --git a/examples/10_tuples_and_destructuring/6_what_we_learned.md b/examples/10_tuples_and_destructuring/6_what_we_learned.md index 18390f7..6ae1fdb 100644 --- a/examples/10_tuples_and_destructuring/6_what_we_learned.md +++ b/examples/10_tuples_and_destructuring/6_what_we_learned.md @@ -9,7 +9,7 @@ You used tuples to return multiple values, destructured them in parameter lists - Build a tuple with parentheses; access fields with `.0`, `.1`, etc. Destructuring with `let (a, b) = pair;` is usually clearer. - Tuples are the lightest-weight way to return more than one value from a function. - When the same tuple shows up in many places or grows past two or three fields, switch to a `struct` (the structs chapter). + When the same tuple shows up in many places or grows past two or three fields, a `struct` can give the fields names. - Use `_` in a pattern to ignore a field: `let (first, _) = pair;`. - Move vs. copy still applies: a tuple of `String`s moves on destructure, a tuple of integers copies. The element types decide. diff --git a/examples/11_option/2_transform.md b/examples/11_option/2_transform.md index 7504729..f565c47 100644 --- a/examples/11_option/2_transform.md +++ b/examples/11_option/2_transform.md @@ -2,7 +2,7 @@ This time you don't want a fallback value, you want to transform whatever is inside the `Option`. You call `.len()` on the inner string when it's `Some`. -A `match` makes both branches explicit; iterator-style methods on `Option` are tidier once you spot them. +A `match` makes both branches explicit, while `Option`'s combinator methods keep this common case shorter. ## Useful from the standard library @@ -12,5 +12,5 @@ A `match` makes both branches explicit; iterator-style methods on `Option` are t - [`Option::map_or`](https://doc.rust-lang.org/std/option/enum.Option.html#method.map_or) collapses both steps into one call: a default for `None` and a closure for `Some`. Reads as `maybe.map_or(0, |s| s.len())`. -- The chapter intro explains the `|s| ...` closure syntax. - For now read it as a tiny one-shot function from `s` to its body. +- You met the `|s| ...` closure syntax in the introduction. + For now, read it as a tiny one-shot function from `s` to its body. diff --git a/examples/11_option/4_find_user.md b/examples/11_option/4_find_user.md index 3c8ab19..b9169f7 100644 --- a/examples/11_option/4_find_user.md +++ b/examples/11_option/4_find_user.md @@ -1,15 +1,8 @@ # Searching a collection -The trickiest of the three: produce an `Option` by searching. -The iterator chapter is still ahead, but `slice::iter()` plus a search combinator already gets you most of the way; the matched tuple still needs to be reduced down to just the username. - -Type walk-through (this is the puzzle): - -- `users.iter()` yields `&(u32, String)` -- `.find(|(uid, _)| *uid == id)` yields `Option<&(u32, String)>` -- `.map(|(_, name)| name.as_str())` yields `Option<&str>` (the return type) - -`name.as_str()` turns the `&String` we destructured out of the tuple into the `&str` the signature wants. +This time you produce an `Option` by searching a slice of user records. +We haven't spent much time with iterators yet, so keep your eye on the types: the search finds a reference to a whole `(u32, String)` tuple, while the function must return only the username as `Option<&str>`. +Your job is to bridge those two types without cloning the `String`. ## Useful from the standard library diff --git a/examples/11_option/5_what_we_learned.md b/examples/11_option/5_what_we_learned.md index f840f7b..30a3d21 100644 --- a/examples/11_option/5_what_we_learned.md +++ b/examples/11_option/5_what_we_learned.md @@ -16,4 +16,4 @@ You consumed `Option`s with fallbacks and combinators, produced new ones from st Use them in tests or when you've already ruled out `None`; otherwise prefer the safer combinators. - The `|x| ...` syntax is a closure: a tiny anonymous function. It shows up everywhere with `Option` and iterators. - The closures chapter covers closures in their own right. + We'll return to closures in their own right later. diff --git a/examples/12_result/1_intro.md b/examples/12_result/1_intro.md index 44ffed6..475ecdb 100644 --- a/examples/12_result/1_intro.md +++ b/examples/12_result/1_intro.md @@ -26,14 +26,14 @@ fn parse_port(input: &str) -> Result { } ``` -The snippet above sneaks in three pieces of syntax that don't have their own chapter, so it's worth pausing on each: +You only need a working reading of three details in this example: `&'static str`, the `::` after `parse`, and the `if` guard on the first match arm. ### `&'static str` This is a `&str` whose lifetime is `'static`: a fancy way of saying "this string lives for the entire duration of the program." String literals like `"port must be greater than 0"` are baked into the binary, so they qualify. For now, treat `&'static str` as the right type to use for hard-coded error messages. -Lifetimes in general get a more careful treatment in later chapters. +We'll build a more complete model of lifetimes later. ### Turbofish: `parse::()` @@ -62,12 +62,9 @@ match n { } ``` -### Back to `Result` +### Handling the two variants -The `&'static str` you see for the error type is the simplest possible error: a borrowed string literal. -Real applications usually define their own error enums, but `&'static str` is fine while you're learning. - -Patterns to handle a `Result`: +As with `Option`, you can handle a `Result` with `match`, `if let`, or a combinator when you have a simple fallback: ```rust match safe_divide(10.0, 0.0) { @@ -83,5 +80,5 @@ if let Ok(n) = safe_divide(10.0, 2.0) { ``` `Result` has many of the same combinators as `Option`: `.map`, `.map_or`, `.and_then`, `.unwrap_or`. -Once you're comfortable with this chapter, the `?` operator (which gets its own chapter) will let you chain fallible operations without the boilerplate. +Next, we'll use the `?` operator to chain fallible operations without writing the same `match` boilerplate each time. diff --git a/examples/12_result/2_safe_divide.md b/examples/12_result/2_safe_divide.md index d1941cf..f9d7caf 100644 --- a/examples/12_result/2_safe_divide.md +++ b/examples/12_result/2_safe_divide.md @@ -7,7 +7,7 @@ The signature is the interesting part: `&'static str` for the error is the simpl ## Useful from the standard library - The `Result` constructors `Ok(value)` and `Err(message)` are in the prelude, so you can use them without importing anything. -- `f64 == 0.0` is the bounds check. - Floating-point comparison has plenty of nasty edge cases in general, but exact zero is fine. +- `f64 == 0.0` detects the failure case. + Floating-point comparison has plenty of nasty edge cases in general, but checking for exact zero is fine here. - [`Result::is_err`](https://doc.rust-lang.org/std/result/enum.Result.html#method.is_err) is what the test uses; you don't need it inside the function. diff --git a/examples/12_result/4_validate_email.md b/examples/12_result/4_validate_email.md index 2f98030..f0a9fa0 100644 --- a/examples/12_result/4_validate_email.md +++ b/examples/12_result/4_validate_email.md @@ -2,7 +2,8 @@ Now the `Ok` value is a borrow of the input. The `&str` in the return type implicitly borrows from `email`, so the compiler infers a lifetime linking input and output via lifetime elision. -The memory and ownership chapter makes this explicit; for now, just notice the function compiles even though no lifetimes appear in the signature. +When we return to memory and ownership, we'll make that lifetime relationship explicit. +For now, notice that the function compiles even though no lifetime appears in the signature. ## Useful from the standard library diff --git a/examples/12_result/5_parse_percentage.md b/examples/12_result/5_parse_percentage.md index 8b7ca44..131ad8f 100644 --- a/examples/12_result/5_parse_percentage.md +++ b/examples/12_result/5_parse_percentage.md @@ -1,9 +1,7 @@ # Parse percentage -This is the hardest function in the chapter; the previous three were warmups. -More than one thing can go wrong, and they need different error messages. -Strip the optional `%` first, then `parse::()` the rest, then bounds-check. -Each step is its own potential `Err`. +A trailing `%` is allowed, but the remaining text may still fail to parse, and a parsed `u8` may be greater than `100`. +Those two failures need different error messages. Note: the error type here is `&'static str`, which means the message has to be a string literal. If you find yourself wanting `format!("{input} is out of range")` in an `Err`, you'd need to change the return type to `Result`. diff --git a/examples/12_result/6_what_we_learned.md b/examples/12_result/6_what_we_learned.md index 1f62a2e..0debbd7 100644 --- a/examples/12_result/6_what_we_learned.md +++ b/examples/12_result/6_what_we_learned.md @@ -1,6 +1,7 @@ # Wrapping up `Result` -You produced `Result`s with simple `if` checks, returned an owned `String` in the `Ok` arm, borrowed from the input via lifetime elision, and combined `strip_suffix`, `parse`, and a bounds check into a real validating parser. +You started with `Result`s built from simple `if` checks, then returned both owned and borrowed success values. +Finally, you combined `strip_suffix`, `parse`, and a bounds check into a validating parser with several failure cases. ## What we learned @@ -9,10 +10,10 @@ You produced `Result`s with simple `if` checks, returned an owned `String` in th - `Ok(value)` and `Err(error)` are the constructors. Both are in the prelude. - `Result` has the same combinator family as `Option`: `unwrap_or`, `map`, `map_or`, plus `map_err` for transforming the error side and `ok` to drop the error and convert to `Option`. -- `&'static str` is the cheapest error type: a borrowed string literal that lives forever. - Real applications usually graduate to enums or `String`-based errors, but this is a fine starting point. +- `&'static str` is a convenient error type when every message is a fixed string literal. + Applications often use error enums or owned `String`s once errors need data of their own. - The turbofish (`parse::()`) spells out a generic type argument at the call site when the type isn't clear from context. - Match guards (`Ok(n) if n > 0 => ...`) attach a boolean condition to a pattern. The arm only fires when both hold. -- The `?` operator (which gets its own chapter) will let you chain fallible calls without writing `match` every time. - For now, `match` is fine. +- Next, we'll use the `?` operator to chain fallible calls without writing `match` every time. + For now, `match` keeps both paths visible. diff --git a/examples/13_question_mark_operator/3_count_file_lines.md b/examples/13_question_mark_operator/3_count_file_lines.md index b1235f6..49f8e1e 100644 --- a/examples/13_question_mark_operator/3_count_file_lines.md +++ b/examples/13_question_mark_operator/3_count_file_lines.md @@ -1,14 +1,15 @@ # `?` with a different error type -Same operator, different error type. -File I/O returns [`std::io::Error`](https://doc.rust-lang.org/std/io/struct.Error.html); the function signature has to declare it as the error type so `?` is happy passing it through. +Here you use the same operator with a different error type. +File I/O returns [`std::io::Error`](https://doc.rust-lang.org/std/io/struct.Error.html), and this function declares that same error type so `?` can pass failures back unchanged. -Notice that `?` doesn't care which error type is involved as long as the function it's used in returns *the same* error type (or one convertible from it via `From`, which is the next step). +`?` doesn't care which concrete error type is involved. +It only needs the surrounding function to return the same error type, or one it can convert into with `From`. ## Useful from the standard library - [`std::fs::read_to_string`](https://doc.rust-lang.org/std/fs/fn.read_to_string.html) reads the whole file into a `String`. - Returns `Result`, which is exactly what `?` wants. + The returned `Result` matches this function's error type. - [`str::lines`](https://doc.rust-lang.org/std/primitive.str.html#method.lines) iterates over the file's lines without keeping the trailing newlines. - [`Iterator::count`](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.count) consumes the iterator and returns how many lines there were. -- The full body fits on one line: `Ok(std::fs::read_to_string(filename)?.lines().count())`. +- Once you have the file contents, `lines()` and `count()` can remain in the same expression. diff --git a/examples/13_question_mark_operator/4_sum_numbers.md b/examples/13_question_mark_operator/4_sum_numbers.md index 0c6717a..c55faa0 100644 --- a/examples/13_question_mark_operator/4_sum_numbers.md +++ b/examples/13_question_mark_operator/4_sum_numbers.md @@ -1,17 +1,17 @@ # `?` through an iterator -`add_parsed_numbers` propagated a single parse error. -This step propagates a whole list of them. +`add_parsed_numbers` had two chances to return a parse error. +`sum_numbers` may inspect many tokens, but it still returns only the first parse error it encounters. `sum_numbers` takes text with integers separated by whitespace and adds them up. The first token that isn't a number makes the function return that `ParseIntError` and stop. -Because the function only parses (no file reading), one error type covers it: no boxing, no conversion. +Since the function only parses, one error type covers every failure without boxing or conversion. -The interesting part is that `?` rides straight through an iterator pipeline. +Here the iterator pipeline produces one `Result`, and `?` either unwraps its total or returns the first error. ## Useful from the standard library - [`str::split_whitespace`](https://doc.rust-lang.org/std/primitive.str.html#method.split_whitespace) yields each token as a `&str`, skipping the gaps between numbers. -- `.map(|token| token.parse::())` turns each token into a `Result`. -- [`Iterator::sum`](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.sum) has an impl that adds a sequence of `Result`s: it returns the first `Err`, or the total wrapped in `Ok`. - So `.sum::>()?` collapses the whole list to an `i32`, or short-circuits on the first bad token. +- Parsing each token turns the iterator into a sequence of `Result` values. +- [`Iterator::sum`](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.sum) can add that sequence of `Result`s, returning the first `Err` or the total wrapped in `Ok`. + Once `sum` has collapsed those results, `?` gives you the total on success. diff --git a/examples/13_question_mark_operator/5_what_we_learned.md b/examples/13_question_mark_operator/5_what_we_learned.md index 427e94d..4a82dbc 100644 --- a/examples/13_question_mark_operator/5_what_we_learned.md +++ b/examples/13_question_mark_operator/5_what_we_learned.md @@ -1,15 +1,15 @@ # Wrapping up the `?` operator -You replaced repetitive `match` chains with `?`, propagated errors out of multi-step functions, and rode `?` straight through an iterator pipeline. +You replaced repetitive `match` chains with `?`, propagated errors out of multi-step functions, and used `?` after an iterator pipeline. ## What we learned - `?` is shorthand for "if this is `Err`, return it from the current function; if it's `Ok`, unwrap the value and keep going." It works on `Option` too (returning `None` early). - The function using `?` must return a `Result` (or `Option`) whose error type matches, or one that the failing error converts into via `From`. -- `?` composes nicely with iterator pipelines: a `.parse()` that returns `Result` slots straight in, and `sum::>()` short-circuits on the first error. +- `?` can follow an iterator pipeline that produces a `Result`, so the first error still returns early. - Every exercise here used a single error type, so `?` propagated with no conversion. When a function genuinely mixes error types (say file I/O and parsing), you need a common error type. - The env-file parser chapter picks that up with `Box`. + We'll pick that up in the env-file parser with `Box`. - Tests that touch the filesystem can race when the harness runs in parallel. Use unique filenames or `cargo test -- --test-threads=1` if you see flaky failures. diff --git a/examples/14_structs_and_methods/1_intro.md b/examples/14_structs_and_methods/1_intro.md index 02a9b50..5e2639f 100644 --- a/examples/14_structs_and_methods/1_intro.md +++ b/examples/14_structs_and_methods/1_intro.md @@ -32,14 +32,14 @@ impl User { } ``` -The three flavors of `self` are the heart of methods: +The form of `self` tells you what access the method receives: -- `&self` reads the struct without modifying it. - Most methods. -- `&mut self` modifies the struct in place. - Requires the caller to have a mutable binding. +- `&self` takes a shared borrow. + You'll use this form for methods that only need to inspect fields. +- `&mut self` takes a mutable borrow, allowing the method to modify the struct in place. + The caller therefore needs a mutable binding. - `self` (no reference) consumes the struct, taking ownership. - Use this when the method returns a transformed version and the original shouldn't be reused. + Choose this form when the method returns a transformed value and the original shouldn't be reused. Field access uses dot notation (`user.name`). Inside `impl` you write `self.field` for the same thing. @@ -49,7 +49,7 @@ Inside `impl` you write `self.field` for the same thing. ## A note on ranges: `0..5` -One of the tests in this chapter calls `record_login()` five times in a loop: +The `record_login` test calls the method five times in a loop: ```rust for _ in 0..5 { diff --git a/examples/14_structs_and_methods/3_display_name.md b/examples/14_structs_and_methods/3_display_name.md index 7ba2d9e..09dd2fd 100644 --- a/examples/14_structs_and_methods/3_display_name.md +++ b/examples/14_structs_and_methods/3_display_name.md @@ -1,16 +1,16 @@ # Methods that borrow `&self` A method taking `&self` reads the struct's fields without modifying or consuming it. -The most common kind. +You'll use this form whenever a method only needs to inspect the value. Inside the method, `self` behaves like any other reference, so you can read fields freely and the caller keeps ownership. `display_name` formats two fields into a new `String`. Use `format!` rather than building the string by hand. -It's the idiomatic tool for this and reads exactly like the format you want. +It reads exactly like the format you want. ## Useful from the standard library - [`format!`](https://doc.rust-lang.org/std/macro.format.html) builds a new `String` from a template and arguments. - Same syntax as `println!`, but returns the string instead of printing it. + It uses the same syntax as `println!`, but returns the string instead of printing it. - Field access uses dot notation: `self.name`, `self.email`. Inside a `format!` template you can interpolate them inline: `format!("{} ({})", self.name, self.email)`. diff --git a/examples/14_structs_and_methods/5_can_access_premium.md b/examples/14_structs_and_methods/5_can_access_premium.md index 2d9ddd3..68503bf 100644 --- a/examples/14_structs_and_methods/5_can_access_premium.md +++ b/examples/14_structs_and_methods/5_can_access_premium.md @@ -8,8 +8,8 @@ In Rust, the body of a function is an expression, so you can just write the bool ## Useful from the standard library -- The `&&` operator short-circuits: if the left side is `false`, the right side isn't evaluated. - Cheap and matches what you'd write in any other language. -- The body is a single expression, so leave off the trailing semicolon. - `self.is_verified && self.login_count >= 5` is a complete function body. -- The expression is idempotent for the caller (`&self`), so it's safe to call as many times as you want without worrying about accidental mutation. +- The `&&` operator short-circuits, so a `false` condition on the left skips the condition on the right. + Here an unverified user doesn't need a login-count check. +- Since the method returns the value of its final expression, leave off the trailing semicolon. + You don't need an explicit `return` for the boolean. +- Taking `&self` means callers can ask the question without giving up ownership or providing a mutable borrow. 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 80c0e61..55b9a33 100644 --- a/examples/14_structs_and_methods/6_what_we_learned.md +++ b/examples/14_structs_and_methods/6_what_we_learned.md @@ -9,10 +9,10 @@ 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 moves and borrowing chapters apply. + These are the same ownership choices you've already made with function parameters. - `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. - `#[derive(Debug, PartialEq)]` covers the common pair: `{:?}` printing for debugging and `==` for tests. - Reach for `Default`, `Clone`, and `Copy` when they fit. + Derive `Default`, `Clone`, or `Copy` only when the generated behavior matches the type. - Encoding business rules as predicates on the type (`user.can_access_premium()`) keeps the rule in one place and makes call sites self-documenting. diff --git a/examples/15_memory_and_ownership/1_intro.md b/examples/15_memory_and_ownership/1_intro.md index 92d0d66..80ea639 100644 --- a/examples/15_memory_and_ownership/1_intro.md +++ b/examples/15_memory_and_ownership/1_intro.md @@ -2,22 +2,23 @@ You've been using ownership for a while now without making a fuss about it. You moved `String`s, borrowed slices, passed `&mut` references into functions, and handed values to `Vec`, `HashMap`, `Option`, `Result`, and your own structs. -This chapter steps back and names what you've been doing, because the pieces add up to the feature Rust is best known for: memory safety without a garbage collector. +Now we can step back and name what you've been doing, because those pieces add up to memory safety without a garbage collector. ## What the borrow checker buys you Languages with manual memory management (C, C++) hand you the power to free memory yourself, and with it the power to free it twice, free it too early, or forget to free it at all. Languages with a garbage collector take that power back and spend runtime and memory tracking what's still alive. -Rust takes a third path: the compiler proves, before the program runs, that every value is freed exactly once and that no reference outlives what it points at. +Rust takes a third path: before the program runs, the compiler checks where owned values are dropped and whether references outlive what they point at. -Three rules make that proof possible, and you've already met all of them: +Three rules summarize the model you've already been using: 1. Every value has exactly one owner. 2. When the owner goes out of scope, the value is dropped. 3. You can borrow a value without owning it, under the aliasing rule (many `&`, or one `&mut`, never both). -Rules 1 and 2 rule out double-frees and leaks: there's always exactly one owner to do the cleanup, and it happens automatically at the end of the scope. -Rule 3 rules out data races and use-after-free: you can't write through one reference while reading through another, and you can't hold a reference to something that's already been dropped. +Rules 1 and 2 give each value one place where cleanup normally happens, which rules out double-frees in safe Rust. +The aliasing part of rule 3 rules out data races because you can't write through one reference while reading through another. +Lifetime checks supply the other half: a reference can't remain usable after its owner has been dropped. The payoff is that "did I free this?" and "is this pointer still valid?" stop being questions you answer at 2am with a debugger. The compiler answers them for you on every build. diff --git a/examples/15_memory_and_ownership/2_lifetimes.md b/examples/15_memory_and_ownership/2_lifetimes.md index 35f7069..65d89b0 100644 --- a/examples/15_memory_and_ownership/2_lifetimes.md +++ b/examples/15_memory_and_ownership/2_lifetimes.md @@ -3,7 +3,7 @@ There's one more thing the borrow checker tracks, and it's the part that scares people by name long before it troubles them in practice: lifetimes. A reference borrows a value, so it must not outlive that value. -If it could, you'd have a reference pointing at memory that's already been cleaned up, the use-after-free bug that rules 1 and 2 exist to prevent. +If it could, you'd have a reference pointing at memory that's already been cleaned up, which is a use-after-free bug. Picture a function that tries to return a reference to its own local string: @@ -22,8 +22,8 @@ Most of the time the compiler works lifetimes out on its own and you never write When you do start annotating them (usually when a struct holds a reference, or a function returns one of several borrowed inputs), the syntax looks heavy, but the question it answers is always the same: which owner does this reference depend on, and will that owner still be alive? You don't need the syntax yet. -For now the goal is to recognize the shape of the error when it shows up and to know it's the same safety rule you've already internalized, seen from a slightly different angle. +For now, recognize the shape of the error and connect it to the same safety rule you've already been using. This is a genuinely hard spot for most people learning Rust. If it hasn't fully clicked, that's expected. -It clicks through use, not through rereading, and you've already done the hard part by feeling where the rules bite. +It usually clicks through use because each compiler error ties the lifetime back to values and scopes you can see. diff --git a/examples/15_memory_and_ownership/3_what_we_learned.md b/examples/15_memory_and_ownership/3_what_we_learned.md index d381a0e..a6868c7 100644 --- a/examples/15_memory_and_ownership/3_what_we_learned.md +++ b/examples/15_memory_and_ownership/3_what_we_learned.md @@ -1,6 +1,6 @@ # Wrapping up -Ownership isn't a single feature you switch on; it's the model underneath everything else in the language. +Ownership is the model underneath the moves, borrows, and references you've been using throughout the course. ## The whole picture @@ -15,7 +15,6 @@ Ownership isn't a single feature you switch on; it's the model underneath everyt ## Why it's worth the friction -Every other memory model asks you to either manage memory by hand and get it right every single time, or accept a garbage collector's runtime cost. -Rust moves that work to compile time. -The borrow checker can be frustrating while you're still learning its rules, but what you get back is a whole class of bugs (use-after-free, double-free, data races) that simply can't reach production. -That trade, an argument with the compiler now instead of a crash later, is the core bet the language makes. +C++ uses RAII and destructors for deterministic cleanup, while garbage-collected languages track which values are still alive at runtime. +Rust adds compile-time ownership and borrowing rules, so safe Rust turns use-after-free, double-free, and data races into compile errors. +The borrow checker can be frustrating while you're still learning its rules, but that trade, an argument with the compiler now instead of a crash later, is the core bet the language makes. diff --git a/examples/16_traits/1_intro.md b/examples/16_traits/1_intro.md index 95db7c0..54d3203 100644 --- a/examples/16_traits/1_intro.md +++ b/examples/16_traits/1_intro.md @@ -30,31 +30,29 @@ Every time you wrote `#[derive(Debug, PartialEq)]` on an enum or struct, you wer That's all `derive` is: a macro that emits the obvious implementation so you don't have to type it out. We'll revisit this in a moment. -## What's in this chapter +## From familiar traits to trait objects -1. **Implementing an existing trait.** You'll write `impl Display for Temperature` so a value formats itself as `"21.5°C"` in `println!` and `format!`. -2. **Defining your own trait.** A `Describable` trait with two implementations, plus a generic function with a *trait bound* that accepts any `T: Describable`. -3. **Default methods.** Traits can ship a default body for a method, which implementors inherit unless they override it. - This is where traits start to feel like mixins. -4. **Trait objects (`dyn Trait`).** Generics give you one specialized copy of a function per concrete type ("static dispatch"). - Trait objects give you one function that dispatches at runtime ("dynamic dispatch") and let you put *different* types into the same `Vec`. +We'll start by implementing `Display`, a trait from the standard library, for a temperature type. +Then we'll define `Describable` and use it as a bound on a generic function. +Default methods will let us share behavior between implementations without repeating it. +Finally, `dyn Trait` will let one collection hold values of different concrete types. -## A quick map of stdlib traits you've already met +## Standard library traits you've already met -| Trait | What it gives you | First seen | +| Trait | What it gives you | Where you know it from | | --- | --- | --- | -| `Debug` | `{:?}` formatting | the enums chapter | -| `Display` | `{}` formatting | this chapter | -| `PartialEq`, `Eq` | `==` and `!=` | the enums chapter | -| `Clone`, `Copy` | `.clone()` and implicit copies | the structs and methods chapter | -| `Default` | `T::default()` | mentioned in passing | -| `Iterator` | `for x in iter`, all the combinators | the iterators chapter | -| `From`, `Into` | `T::from(x)` and `x.into()` conversions | sprinkled throughout | +| `Debug` | `{:?}` formatting | enums | +| `Display` | `{}` formatting | the exercises below | +| `PartialEq`, `Eq` | `==` and `!=` | enums | +| `Clone`, `Copy` | `.clone()` and implicit copies | structs and methods | +| `Default` | `T::default()` | earlier mentions | +| `Iterator` | `for x in iter`, all the combinators | the iterator pipelines we'll use next | +| `From`, `Into` | `T::from(x)` and `x.into()` conversions | earlier conversions | None of those are magic. -Each is a regular trait defined in `std`, and the standard library `impl`s it for the obvious built-in types. -When you `derive` one, the compiler writes the impl. -When you can't derive (maybe the auto-generated version isn't what you want), you write `impl Display for MyType { ... }` by hand, just like in step 2. +Each is a regular trait defined in `std`, with implementations for the built-in types where they make sense. +When you `derive` one, the compiler writes the implementation. +When the generated behavior isn't what you want, you write the implementation by hand. ## Static vs. dynamic dispatch: a sneak preview @@ -70,5 +68,5 @@ fn print_all(items: &[T]) { /* ... */ } fn print_all_dyn(items: &[&dyn Display]) { /* ... */ } ``` -You'll write both in this chapter. -The `Box` form, which solves the "but trait objects don't have a known size" problem you'll bump into, is the headline act of the smart pointers chapter. +You'll use both forms in the exercises below. +When we get to the optional smart pointers material, we'll unpack how `Box` gives an unsized trait object an owning, fixed-size handle. diff --git a/examples/16_traits/3_describable.md b/examples/16_traits/3_describable.md index 2855b03..d4d675a 100644 --- a/examples/16_traits/3_describable.md +++ b/examples/16_traits/3_describable.md @@ -26,7 +26,7 @@ This is Rust's answer to "polymorphism." The compiler stamps out one specialized copy of `print_one` per type you call it with (called *monomorphization*; the C++ template crowd will feel at home). There's no runtime dispatch and no boxing. -A few variations you'll meet in real code, just so you recognize them: +Real code often spells the same kind of bound in one of these forms: ```rust // Multiple bounds with `+`: @@ -46,12 +46,12 @@ where fn show(item: &impl Describable) { /* ... */ } ``` -You'll write the simple `` form in this step. -The others are sugar for the same underlying machinery. +For this exercise, use the simple `` form. +You only need to recognize the others as different spellings of the same machinery. ## Useful from the standard library - [The Rust Book on traits](https://doc.rust-lang.org/book/ch10-02-traits.html) walks through definitions, implementations, and bounds with more examples than fit here. - `Vec::join("\n")` (and any `&[String].join(...)`) is handy for the `print_descriptions` exercise: build a `Vec` of per-item descriptions, then join them with newlines. - The standard `Iterator::map` plus `.collect::>()` is the idiomatic way to turn a `&[T]` into a `Vec`. - You met both in passing; the iterators chapter covers iterators properly. + You met both in passing, and we'll spend more time with them when we get to iterators. diff --git a/examples/16_traits/4_logger.md b/examples/16_traits/4_logger.md index 3bf1e78..915ba5d 100644 --- a/examples/16_traits/4_logger.md +++ b/examples/16_traits/4_logger.md @@ -21,10 +21,10 @@ This is how `Iterator` gets away with offering dozens of methods (`map`, `filter Every other method is a default body written in terms of `next`. Haskellers will recognise the pattern from type class default methods; Java added the same feature as "default methods on interfaces" in Java 8. -## What this step adds +## A logger with shared behavior -You'll work with a small `Logger` trait. -There's exactly one required method (`log`, which formats a single line) and two default methods (`warn` and `error`) that build on top of it. +The `Logger` trait has one required method, `log`, which formats a single line. +Its `warn` and `error` methods provide default bodies built on top of `log`. ```rust trait Logger { @@ -40,21 +40,21 @@ trait Logger { } ``` -This is deliberately recognisable: every logging library in every language has this same skeleton. -The teaching value of the default methods is that they live on the *trait*, so adding a new implementor doesn't mean writing `warn` and `error` from scratch every time. +The shape should look familiar from logging APIs in other languages. +Because `warn` and `error` live on the *trait*, each new implementor gets both without writing them again. -Two implementors: +You'll implement that contrast with two types: 1. `PlainLogger` returns the message untouched. It uses both defaults as written, so all you have to write is `log`. 2. `TaggedLogger { tag: String }` prepends a tag (e.g. `"auth: ..."`). It uses the default `warn`, but *overrides* `error` to swap the `[ERROR]` prefix for a louder `[CRITICAL]` prefix. -That asymmetry is the point: implementors take what they want from the defaults and override only the bits they need to change. +That asymmetry lets each implementor keep the defaults that fit and replace only the behavior that differs. ## Useful from the standard library - The `format!` macro is the workhorse here. Default `warn` builds `"[WARN] {msg}"` and hands it back to `self.log`, so whatever decoration `log` does (the tag, in `TaggedLogger`'s case) wraps the warning prefix. - Default methods are written *inside* the `trait` block, with a body instead of a trailing semicolon. - The required methods (the ones without a body) are still mandatory; defaults are bonus. + A method ending in a semicolon remains required, while a method with a body can be inherited or overridden. diff --git a/examples/16_traits/5_validate.md b/examples/16_traits/5_validate.md index 5aeb2e7..bd04e24 100644 --- a/examples/16_traits/5_validate.md +++ b/examples/16_traits/5_validate.md @@ -6,7 +6,7 @@ You can pass `&[Book]` or `&[Movie]`, but not a slice that contains *both*. That's because the compiler picks one `T` per call site and produces a specialized copy of the function for it. The slice type `&[T]` has to agree on a single element type, and two different structs are two different types as far as the type system is concerned. -## Enter `dyn Trait` +## Using `dyn Trait` When you want a single collection that holds *different* concrete types as long as they all implement the same trait, you reach for a **trait object**, spelled `dyn Trait`: @@ -18,13 +18,11 @@ fn run_all(items: &[&dyn Validator], input: &str) { } ``` -Two things changed compared to the generic version: - -1. **Element type.** `&dyn Validator` is a *fat pointer*: two words that together point at the value and at a vtable of function pointers (one per trait method). - At each call to `.check(...)`, Rust looks the function up in the vtable. - C++ folks: this is the same machinery as virtual methods, just opt-in per call site instead of per class. -2. **One function, not many.** `run_all` is compiled exactly once. - The price is the indirection through the vtable; the payoff is heterogeneous collections. +`&dyn Validator` is a *fat pointer*: two words that point to the value and to a vtable of function pointers, one for each trait method. +At each call to `.check(...)`, Rust looks up the function in that vtable. +C++ folks will recognize the same machinery as virtual methods, but here you opt into it at the call site instead of on the class. +`run_all` is also compiled exactly once rather than once per concrete type. +That trades one vtable lookup per call for the ability to mix concrete types in the slice. ## Static vs. dynamic dispatch, side by side @@ -38,10 +36,10 @@ Two things changed compared to the generic version: Neither is "better." Use generics by default for performance and flexibility, and reach for `dyn Trait` when you genuinely need heterogeneous storage or want a smaller binary. -## What this step adds +## A validation example -You'll build a tiny composable validation library. -The trait is deliberately small: +You'll use trait objects to build a small validation library. +Each validator needs one method: ```rust trait Validator { @@ -50,20 +48,20 @@ trait Validator { } ``` -Three implementors, each enforcing one rule: +You'll give three structs one rule each: - `MinLength { n }`: input must have at least `n` characters. - `MustContain { needle }`: input must contain the given substring. - `MustNotContain { forbidden }`: input must *not* contain the given substring. -The killer feature is that `MinLength`, `MustContain`, and `MustNotContain` are three different types, but a single `&[&dyn Validator]` slice can hold all of them at once. -That's what makes "pluggable rules" work: every rule is a different struct (and might carry different configuration), but the call site only knows "a list of things that can validate." +`MinLength`, `MustContain`, and `MustNotContain` are different types, but one `&[&dyn Validator]` slice can hold all three. +Each rule can carry its own configuration while the call site only needs a list of values that can validate. -You'll see the same idea, scaled up, in the password validator chapter: a configurable set of checks running against one input. +If you take the optional password validator later, you'll use the same idea for a configurable set of checks. ## A word about `Box` -You'll see `Box` everywhere in real Rust code: +You'll also see `Box` in Rust code: ```rust let rules: Vec> = vec![ @@ -74,12 +72,12 @@ let rules: Vec> = vec![ The reason: `dyn Trait` has no statically known size (the three implementors above can carry different fields, so they don't all take up the same number of bytes), so the compiler won't let you put bare `dyn Validator` values directly in a `Vec`. A `Box` is a heap allocation with a fixed-size pointer that lives on the stack, which sidesteps the size problem. -That's exactly what the smart pointers chapter unpacks. -For this step we sidestep it with `&dyn Validator` references, which are also fixed-size and work fine for borrowing. +We'll unpack that ownership pattern in the optional smart pointers material. +For now, `&dyn Validator` references give us the same fixed-size handle without taking ownership. ## Useful from the standard library - [The Rust Book on trait objects](https://doc.rust-lang.org/book/ch18-02-trait-objects.html). - `str::contains` (with a `&str` argument) is all you need for the `MustContain` / `MustNotContain` checks. - Inside `collect_errors`, a plain `for` loop pushing into a `Vec` is the most direct form. - The same chain with `.iter().filter_map(...)` works once you've met iterators in the iterators chapter. + Once we get to iterators, the same loop can become an `.iter().filter_map(...)` chain. diff --git a/examples/16_traits/7_what_we_learned.md b/examples/16_traits/7_what_we_learned.md index 49a1628..e3c098c 100644 --- a/examples/16_traits/7_what_we_learned.md +++ b/examples/16_traits/7_what_we_learned.md @@ -1,6 +1,8 @@ # Wrapping up traits -You implemented an stdlib trait (`Display`), defined your own (`Describable`), wrote a generic function with a trait bound, gave a trait a default method that one type inherited and another overrode (the `Logger` step), and finally swapped a generic for a trait object so a single slice could hold a mix of validation rules. +You implemented the standard library's `Display` trait and defined a `Describable` trait of your own. +You used `Describable` as a generic bound, then shared behavior through default methods. +Finally, you switched from a generic to a trait object so one slice could hold several kinds of validation rule. ## What we learned @@ -16,15 +18,11 @@ You implemented an stdlib trait (`Display`), defined your own (`Describable`), w `fn f(x: &T)` is the basic form, `T: A + B` combines bounds, and `where` clauses let you push long bounds out of the signature. - **Default methods** in the trait body give every implementor a baseline behavior. Override per type when you need to. -- **Generics** dispatch statically: one specialized copy of the function per concrete `T`. - Fast, but one call can only see one concrete type. - Great default. +- **Generics** dispatch statically, so the compiler creates one specialized copy of the function per concrete `T`. + Use them when each call only needs one concrete type. - **Trait objects (`dyn Trait`)** dispatch dynamically through a vtable. - Slower per call but lets one slice or `Vec` hold values of multiple concrete types at once. - Reach for them when you need heterogeneity. + Use them when one slice or `Vec` needs to hold several concrete types at once. - `Box` solves the "trait objects have no known size" problem so they can live in owning containers like `Vec`. - That's the headline of the optional **smart pointers** bonus chapter (`Box`, `Rc`, `RefCell`). - Dip into it whenever you want to go deeper. -- Many stdlib traits you already use (`Iterator`, `From`, `Into`, `PartialEq`, ...) are just regular traits. - Nothing magic. - You can define your own or implement the standard ones for your own types. + We'll unpack that pattern alongside `Box`, `Rc`, and `RefCell` in the optional **smart pointers** material. +- Many standard library traits you already use (`Iterator`, `From`, `Into`, `PartialEq`, ...) follow the same rules. + You can define traits of your own or implement standard traits for your own types. diff --git a/examples/17_iterators/3_sum.md b/examples/17_iterators/3_sum.md index 9194797..8cec357 100644 --- a/examples/17_iterators/3_sum.md +++ b/examples/17_iterators/3_sum.md @@ -1,10 +1,10 @@ # Summing with an iterator -Iterators were popularised by functional languages like Lisp (created by John McCarthy in 1958), and today they're a core building block in most modern languages. -Rust's iterators are lazy: they don't do any work until you ask for a result. +A running total is a good first place to see what an iterator consumer does. +Rust's iterators are lazy, so they don't do any work until you ask for a result. -The simplest pattern is to take a sequence and collapse it down to a single value. -You could write a `for` loop with a running total, but the standard library can do this for you in one call. +You could add the values with a `for` loop and an accumulator. +Here, `sum` asks the iterator for each value and collapses the sequence into one total. ## Useful from the standard library diff --git a/examples/17_iterators/4_map.md b/examples/17_iterators/4_map.md index 21deb90..e7838d9 100644 --- a/examples/17_iterators/4_map.md +++ b/examples/17_iterators/4_map.md @@ -1,7 +1,7 @@ # Transforming with `map` Now you need to transform every element instead of collapsing the sequence. -The pattern is `vec.into_iter()` -> some combinator that applies a closure -> back to a `Vec` via `collect()`. +Read this pipeline from left to right: take ownership of the vector's items, transform each one, then collect the results into a new vector. `map` is lazy: it just describes the transformation. Nothing runs until `collect` (or another consumer) asks for the results. diff --git a/examples/17_iterators/5_filter.md b/examples/17_iterators/5_filter.md index 4b94dd8..ca8172d 100644 --- a/examples/17_iterators/5_filter.md +++ b/examples/17_iterators/5_filter.md @@ -1,10 +1,10 @@ # Keeping elements with `filter` -Same idea as `map`, but instead of transforming each element you keep some and drop others. -Watch out for one borrowing gotcha: `.iter()` yields `&T`, but `filter` gives its closure another reference on top, so the closure sees `&&T`. +With `map` you transformed every item, while `filter` keeps some items and drops the rest. +There is one borrowing detail to watch: `.iter()` yields `&T`, but `filter` gives its closure another reference on top, so the closure sees `&&T`. That's why you'll often see `**c == ...` or `s.starts_with(...)` (which auto-derefs) instead of plain `c == ...`. -Don't be alarmed when the compiler complains about a missing `&`, see [the cheatsheet](/cheatsheet) entry on iterators. +If the compiler reports a missing `&`, the [iterators entry in the cheatsheet](/cheatsheet) shows the reference layers side by side. ## Useful from the standard library diff --git a/examples/17_iterators/6_filter_to_string.md b/examples/17_iterators/6_filter_to_string.md index 48f2f3f..b728f64 100644 --- a/examples/17_iterators/6_filter_to_string.md +++ b/examples/17_iterators/6_filter_to_string.md @@ -1,9 +1,9 @@ # Filter, then own the result -Same as the previous step, but the input is a `&[&str]` (a borrowed slice of borrowed strings), so the iterator yields `&&str`. +This time the input is a `&[&str]`, a borrowed slice of borrowed strings, so the iterator yields `&&str`. We sidestep that double-reference by returning owned `String`s; the lesson here is iterators, not lifetimes. -To go from `&&str` to `String`, reach for [`str::to_string`]. +[`str::to_string`](https://doc.rust-lang.org/std/primitive.str.html#method.to_string) converts each surviving `&&str` into an owned `String` through auto-deref. Chain it after your `filter` with a `map`, then `collect` into a `Vec`. ## Useful from the standard library @@ -11,6 +11,6 @@ Chain it after your `filter` with a `map`, then `collect` into a `Vec`. - [`Iterator::filter`](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.filter) again. Same closure structure; auto-deref still saves you for `.ends_with(".rs")`. - [`Iterator::map`](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.map) is what converts the surviving `&&str`s into owned `String`s. -- [`str::to_string`](https://doc.rust-lang.org/std/primitive.str.html#method.to_string) is the easy `&str` -> `String` call. +- [`str::to_string`](https://doc.rust-lang.org/std/primitive.str.html#method.to_string) converts a borrowed string slice into an owned `String`. Auto-deref reaches through the extra reference for you. - [`str::ends_with`](https://doc.rust-lang.org/std/primitive.str.html#method.ends_with) is the suffix check used by the predicate. diff --git a/examples/17_iterators/7_what_we_learned.md b/examples/17_iterators/7_what_we_learned.md index ca5247e..a46391f 100644 --- a/examples/17_iterators/7_what_we_learned.md +++ b/examples/17_iterators/7_what_we_learned.md @@ -4,7 +4,9 @@ You collapsed a numeric vector with `sum`, transformed every element with `map`, ## What we learned -- An iterator is a pipeline: get one with `.iter()` / `.iter_mut()` / `.into_iter()` (or directly from things like `.chars()` and `.lines()`), chain lazy adapters, then finish with a consumer. +- An iterator pipeline starts with `.iter()`, `.iter_mut()`, `.into_iter()`, or a method such as `.chars()` or `.lines()`. + Lazy adapters describe what should happen to each item. + A consumer finishes the pipeline by asking for results. - `iter` yields `&T`, `iter_mut` yields `&mut T`, `into_iter` moves out of the collection and yields `T`. Pick the one that matches what you intend to do with each item. - Adapters (`map`, `filter`, `take`, `skip`, ...) describe the pipeline but do nothing on their own. diff --git a/examples/18_word_frequencies/1_intro.md b/examples/18_word_frequencies/1_intro.md index c5a8d83..e99a713 100644 --- a/examples/18_word_frequencies/1_intro.md +++ b/examples/18_word_frequencies/1_intro.md @@ -2,14 +2,11 @@ Time to extend our running word-count example. Back in the word count chapter you built `word_count`, `char_count`, and `longest_word` with simple `for` loops. -The iterators chapter then showed how iterators collapse those into one-liners. -This chapter goes one level deeper: instead of asking *how many* words a text has, we'll ask *which* words appear and *how often each one* shows up. +Then you used iterators to collapse those loops into one-liners. +Now we'll go one level deeper: instead of asking *how many* words a text has, we'll ask *which* words appear and *how often each one* shows up. -There's no big new concept here either. -It puts iterators, hashmaps, and `Option` to work together. -Along the way you'll meet two new iterator tricks (`max_by_key` and `HashMap::into_iter`); the rest is just applying what's already in your toolbox. - -A few patterns you'll likely use: +We're putting iterators, hashmaps, and `Option` to work together. +The only new iterator tools are `max_by_key` and `HashMap::into_iter`, so you can focus on how the pieces connect. **Splitting text into words.** Both `split_whitespace` and `split` return iterators of `&str`. The first handles any kind of whitespace and skips empties, which is usually what you want for natural text: @@ -36,7 +33,7 @@ let top = counts.iter().max_by_key(|(_, count)| *count); // top: Option<(&String, &usize)> ``` -**Computing an average.** Sum the lengths, divide by the count, watch out for the integer-division trap: +**Computing an average.** Add the lengths, convert the totals to `f64`, and only then divide so integer truncation can't discard the fraction: ```rust let total_chars: usize = words.iter().map(|w| w.len()).sum(); diff --git a/examples/18_word_frequencies/2_count_words.md b/examples/18_word_frequencies/2_count_words.md index 6047e72..95cd952 100644 --- a/examples/18_word_frequencies/2_count_words.md +++ b/examples/18_word_frequencies/2_count_words.md @@ -1,15 +1,15 @@ # Counting words -The foundation for everything else in this chapter: take a string of text and produce a `HashMap` that maps each word to how many times it appears. +Start by turning a string of text into a `HashMap` that records how many times each word appears. Words are separated by whitespace and the count should be case-insensitive: `"Hello"` and `"hello"` are the same word. -The classic recipe is: split on whitespace, lowercase each piece, then walk the resulting iterator and bump a counter in the map. -The `entry` API on `HashMap` is the idiomatic way to do that last step: `*map.entry(key).or_insert(0) += 1`. +Build the map by splitting on whitespace, lowercasing each piece, and bumping its counter. +The `entry` API handles the lookup and default insertion together, then gives you the counter to update. ## Useful from the standard library - [`str::split_whitespace`](https://doc.rust-lang.org/std/primitive.str.html#method.split_whitespace) splits on any whitespace and skips empty pieces. - Almost always what you want for word splitting. + That makes it a better default for natural text than splitting on one literal space. - [`str::to_lowercase`](https://doc.rust-lang.org/std/primitive.str.html#method.to_lowercase) returns a fresh `String`. Use it as the map key so `Hello` and `hello` collapse together. -- [`HashMap::entry`](https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.entry) + [`Entry::or_insert`](https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html#method.or_insert) is the "look up; insert default; mutate" pattern from the hashmaps chapter. +- [`HashMap::entry`](https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.entry) + [`Entry::or_insert`](https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html#method.or_insert) is the "look up; insert default; mutate" pattern you used with hashmaps. diff --git a/examples/18_word_frequencies/3_most_common_word.md b/examples/18_word_frequencies/3_most_common_word.md index 9076694..45175d3 100644 --- a/examples/18_word_frequencies/3_most_common_word.md +++ b/examples/18_word_frequencies/3_most_common_word.md @@ -1,17 +1,17 @@ # The most common word Now that you can count, finding the maximum is a one-liner, almost. -The borrow checker has an opinion about returning data out of a `HashMap`, and that's the real lesson of this step. +The choice between `iter` and `into_iter` determines whether you can return the winning word without cloning it. `count_words` is duplicated below as a `todo!()` stub so this step compiles in isolation; you don't need to fill it in again. -Focus on `most_common_word`. -Once you have it, the test will drive both through `unwrap()`. +You only need to work on `most_common_word`. +The test will call both functions and unwrap the result. ## Useful from the standard library - [`HashMap::into_iter`](https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.into_iter) consumes the map and yields owned `(K, V)` pairs. That's how you get an owned `String` out without cloning. - [`Iterator::max_by_key`](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.max_by_key) returns the entry with the largest derived key as an `Option`. - `max_by_key(|(_, count)| *count)` does the trick here. + Use the count half of each `(word, count)` pair as that key. - An empty input naturally produces `None`: `count_words` returns an empty map, `into_iter().max_by_key(...)` returns `None`, and the function signature already says `Option<(String, usize)>`. - No special case needed. + You don't need an extra branch for that case. diff --git a/examples/18_word_frequencies/4_text_stats.md b/examples/18_word_frequencies/4_text_stats.md index 8a1b945..7f66f87 100644 --- a/examples/18_word_frequencies/4_text_stats.md +++ b/examples/18_word_frequencies/4_text_stats.md @@ -1,12 +1,12 @@ # Text statistics -The orchestrator step, and the one with the most aggregations in a single body. +Now you'll combine several small aggregations in one function. `text_stats` returns three numbers about a piece of text: total word count, number of unique words, and the average word length as an `f64`. You can compute all three from a single pass over `count_words`'s result, or split the work; either is fine. The average is where Rust makes you slow down. Integer division truncates, so cast to `f64` before you divide, not after. -And once the result is a float, the test can't check it with `==`: floats don't land on exact values, so it compares against a small tolerance instead. +The test compares the result against a small tolerance because calculations with `f64` can introduce rounding error. `count_words` is stubbed with `todo!()` again so this file compiles on its own. Wire `text_stats` up however you like. @@ -14,8 +14,8 @@ The test only cares about the returned tuple. ## Useful from the standard library -- The total word count is the sum of every value in the map: `counts.values().sum::()`. -- The unique-word count is `counts.len()`. -- For the average length, sum `key.chars().count() * count` across the map (or sum `word.len()` straight from a fresh `text.split_whitespace()` pass) and divide by the total. - Watch the integer-division trap: cast both operands to `f64` before the divide. +- Each map value is an occurrence count, so adding the values gives you the total number of words. +- The map's length gives you the number of unique words because each key appears once. +- For the average length, account for both the length of each word and the number of times it occurred. + Convert the total characters and total words to `f64` before dividing. - [`HashMap::values`](https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.values) and [`HashMap::iter`](https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.iter) are the two iterator entry points you'll likely use here. diff --git a/examples/18_word_frequencies/5_what_we_learned.md b/examples/18_word_frequencies/5_what_we_learned.md index b1671d3..e5786e5 100644 --- a/examples/18_word_frequencies/5_what_we_learned.md +++ b/examples/18_word_frequencies/5_what_we_learned.md @@ -7,15 +7,15 @@ You glued together the chapters so far: a `HashMap` keyed by lowercased words, a - `split_whitespace()` is the right default for word-splitting in natural text. It collapses runs of whitespace and skips empties. - Lowercasing keys (or any other normalization step) belongs to the same pipeline that builds the map, not to the consumer side. -- `into_iter` on a `HashMap` is the standard escape hatch when you need to return owned data out of it. - `iter` only hands out borrows. +- `into_iter` transfers the keys and values out of a `HashMap`, which lets you return owned data without cloning it. + In contrast, `iter` only lends you references to entries that remain in the map. - `max_by_key` returns an `Option`, so empty input naturally collapses to `None` without a special-case branch. - Watch the integer-division trap when computing averages: divide *after* casting to `f64`, not before. - `f64` comparisons need a tolerance (`(a - b).abs() < eps`); never `==`. + Tests for calculated `f64` values usually compare a tolerance such as `(a - b).abs() < eps` instead of using `==`. - Tuples like `(usize, usize, f64)` work for tiny ad-hoc returns, but a named struct (`TextStats { total, unique, avg_len }`) reads better at the call site as soon as a function takes off in scope. ## An optional detour You now have every tool you need to build a small program from scratch: structs, enums, iterators, `Option`, `Result`, vectors, and strings. -There's an optional **Creative Break** chapter, an open-ended password validator project rather than a guided lesson. -It isn't part of the main sequence and nothing later depends on it, so take it whenever you want a change of pace, or skip straight ahead. +If you want a change of pace, the optional **Creative Break** is an open-ended password validator project rather than a guided lesson. +Nothing later depends on it, so you can take the detour now or keep going. diff --git a/examples/19_password_validator/1_intro.md b/examples/19_password_validator/1_intro.md index 556f72e..ccb0180 100644 --- a/examples/19_password_validator/1_intro.md +++ b/examples/19_password_validator/1_intro.md @@ -2,19 +2,19 @@ By now you've picked up enough Rust to write a lot of genuinely useful programs. -This chapter is an open-ended project rather than a focused lesson. -You already have the tools you need: structs, enums, iterators, `Option`, `Result`, vectors, and strings. -The fun part is putting them together. +This time, instead of adding one new concept, you'll combine the tools you already have in an open-ended project. +You already know the pieces: structs, enums, iterators, `Option`, `Result`, vectors, and strings. +The fun part is deciding how they fit together. -From this chapter onward the files get longer, and the in-browser editor starts feeling cramped. -You have two upgrades available: +The files get longer from here, and the in-browser editor may start to feel cramped. +When that happens, you can open the same work in a roomier editor: - **Open in Web Editor** (the button above each editor) opens the current file on [github.dev](https://github.dev/corrode/course): a full browser-based VS Code with proper find-in-file, multi-cursor, and the keyboard shortcuts you'd expect. No install, no clone. - **Run it locally.** Clone [the repo](https://github.com/corrode/course), open a chapter under `examples/NN_slug/`, and run `cargo test --example NN_slug` (or `cargo check` for a faster compile-only loop). Local gets you `rust-analyzer`, on-save formatting, and the proper Rust workflow you'll want once you start writing real projects. -## A few patterns that come up +## Patterns you can reuse **Counting with iterators.** The `.filter(...).count()` combo is a quick way to ask "how many of these match?": @@ -70,8 +70,8 @@ For real randomness, the [`rand`](https://docs.rs/rand) crate is the standard an ## Ideas to try -The three steps that follow give you a warm-up (`is_strong`), a generator, and the scoring engine (`validate`). -Once the tests pass, the chapter is yours to extend: +Start with the `is_strong` warm-up, then move on to the generator and scoring engine. +Once those tests pass, take the validator in any direction that sounds interesting: - Turn the validator's terse feedback (`"too short"`, `"missing digit"`) into friendly advice (`"Add at least 4 more characters"`). - Detect common passwords, repeated runs (`aaa`, `111`), or keyboard walks (`qwerty`, `123456`) and dock points for them. diff --git a/examples/19_password_validator/2_hints.md b/examples/19_password_validator/2_hints.md index e5d4717..17e685b 100644 --- a/examples/19_password_validator/2_hints.md +++ b/examples/19_password_validator/2_hints.md @@ -1,6 +1,6 @@ # Hints -This chapter is open-ended. +There are several reasonable ways to build this project. The hints below are scaffolding, not a solution. They're here to keep you moving when you're stuck on *where to start*, not on *which trick to use*. diff --git a/examples/19_password_validator/3_is_strong.md b/examples/19_password_validator/3_is_strong.md index f6d600d..00ec429 100644 --- a/examples/19_password_validator/3_is_strong.md +++ b/examples/19_password_validator/3_is_strong.md @@ -1,13 +1,12 @@ # Warm-up: `is_strong` -Welcome to the open-ended chapter. -The whole exercise revolves around a `PasswordReport` value: a structured verdict about a password, with a numeric score, some human-readable feedback, and a coarse strength label. +The project revolves around a `PasswordReport` value: a structured verdict about a password, with a numeric score, some human-readable feedback, and a coarse strength label. -We start small. +We'll start small. Before tackling the actual scoring, get a feel for the data by implementing the one-line `is_strong` method on `PasswordReport`. By convention in this exercise, "strong" means the score is at least `70`. -This step also introduces the shared `PasswordStrength` enum and `PasswordReport` struct that every later step will reuse (each step re-declares them so it can stand on its own). +Here you'll also meet the shared `PasswordStrength` enum and `PasswordReport` struct; each later page re-declares them so it can run on its own. ## Useful from the standard library diff --git a/examples/19_password_validator/5_validate.md b/examples/19_password_validator/5_validate.md index c417313..7d9481c 100644 --- a/examples/19_password_validator/5_validate.md +++ b/examples/19_password_validator/5_validate.md @@ -1,6 +1,6 @@ # The orchestrator: `validate` -Time to combine everything. +Now the character checks, scoring rules, and feedback come together. `PasswordValidator::validate(password)` returns a `PasswordReport` with a numeric score, a list of feedback messages, and a `PasswordStrength` label. The shared types and the four `has_*` character-class helpers are stubbed below. diff --git a/examples/19_password_validator/6_what_we_learned.md b/examples/19_password_validator/6_what_we_learned.md index 5398107..74950d6 100644 --- a/examples/19_password_validator/6_what_we_learned.md +++ b/examples/19_password_validator/6_what_we_learned.md @@ -4,14 +4,11 @@ You've put the whole standard toolkit to work in one project: a struct, an enum, ## What we learned -- Open-ended projects are where the chapters since the integers chapter start to feel cohesive. - The same handful of types (struct, enum, `Vec`, `String`, `Option`) keep showing up. -- Per-character checks are almost always `s.chars().any(|c| c.is_ascii_*())` or `s.chars().filter(...).count()`. - Internalise this call chain. +- Here, tools you first met separately work together: structs and enums describe the result, iterators inspect the password, and a `Vec` collects feedback. +- Use `s.chars().any(|c| c.is_ascii_*())` for a yes-or-no question and `s.chars().filter(...).count()` when you need the number of matches. - Membership in a small set of literal characters is one `"!@#$%^&*".contains(c)` call. No need for a `HashSet`. -- A `Vec` you `push` into as you check each rule is the idiomatic way to accumulate validation feedback. -- Range patterns inside `match` arms (`0..30 => Weak`) are the cleanest way to bucket a number into categories. +- Range patterns inside `match` arms (`0..30 => Weak`) bucket a number into categories without a chain of `if`/`else` checks. - Splitting a domain across small types (`PasswordReport`, `PasswordStrength`, `PasswordValidator`) keeps each piece focused on one job and easy to test. - For real randomness, reach for the [`rand`](https://docs.rs/rand) crate. The clock-based trick is fine for an exercise, never for a password generator that ships. diff --git a/examples/20_modules_and_visibility/1_intro.md b/examples/20_modules_and_visibility/1_intro.md index cb4f752..d6326d2 100644 --- a/examples/20_modules_and_visibility/1_intro.md +++ b/examples/20_modules_and_visibility/1_intro.md @@ -1,7 +1,7 @@ # Modules and visibility Modules are how Rust organizes code into namespaces. -They give you two things: a way to group related items together, and a way to control which of those items are visible from the outside. +They let you group related items and control which ones outside code can use. The default is private. Add `pub` to expose something: @@ -24,11 +24,12 @@ fn main() { You can declare a module inline (as above) or in a separate file. The syntax `mod foo;` (no body) tells the compiler to look for `foo.rs` or `foo/mod.rs` next to the current file. -## Visibility for fields and variants +## Visibility for struct fields Marking a `struct` `pub` only makes the *type* public. -The fields are still private unless individually marked. -Same for `enum` variants: +Its fields are still private unless you mark them individually. +Enums work differently: once an enum is public, its variants are public too. +For a struct, you choose field by field: ```rust mod config { @@ -45,8 +46,8 @@ mod config { } ``` -This is how you build clean APIs: expose the bare minimum (constructors, methods, sometimes a few fields), keep everything else private. -Callers can't reach into your internals, so you're free to refactor them later. +A narrow public surface keeps callers from depending on details you may want to change later. +Expose the constructors, methods, and fields they need, and leave the rest private. ## Path syntax @@ -58,5 +59,5 @@ Callers can't reach into your internals, so you're free to refactor them later. ## Useful resources - [The Rust Book on modules](https://doc.rust-lang.org/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html) is the long-form reference, including how packages and crates fit in. -- [The Rust Reference on visibility](https://doc.rust-lang.org/reference/visibility-and-privacy.html) is the precise rules, when you need them. +- [The Rust Reference on visibility](https://doc.rust-lang.org/reference/visibility-and-privacy.html) has the precise rules for when you need them. - [`pub(crate)`](https://doc.rust-lang.org/reference/visibility-and-privacy.html#pubin-path-pubcrate-pubsuper-and-pubself) is a useful middle ground: visible everywhere in your crate, hidden from external users. diff --git a/examples/20_modules_and_visibility/4_settings.md b/examples/20_modules_and_visibility/4_settings.md index b72a6a9..8db1c89 100644 --- a/examples/20_modules_and_visibility/4_settings.md +++ b/examples/20_modules_and_visibility/4_settings.md @@ -1,6 +1,6 @@ # Public type, private fields -This is the real visibility lesson, and it catches almost everyone the first time. +Making the type public is only the first layer. `pub struct Settings` makes the *type* visible outside its module. It does *not* make the fields public. @@ -8,11 +8,11 @@ It does *not* make the fields public. So a caller outside the module cannot write `settings.port`. That line fails to compile. -The supported path is a `pub` accessor like `get_port`, and that is the whole reason the pattern exists: you expose a stable method and keep the field free to change later. +A public accessor such as `get_port` gives callers a stable path to the value while the field remains free to change. The same opt-in rule covers methods. `new` and `get_port` are private until you `pub` each one, so this step is broken in more than one place. Compile, read the error, `pub` the item it names, and repeat until the compiler runs out of complaints. When you want something between fully public and fully private, `pub(crate)` makes an item visible everywhere in your own crate while keeping it hidden from outside users. -That is the usual choice for helpers several modules share but that aren't part of your public API. +Use it for helpers that several modules share but that aren't part of your public API. diff --git a/examples/21_environment_file_parser/1_intro.md b/examples/21_environment_file_parser/1_intro.md index ee9c0ce..f6db319 100644 --- a/examples/21_environment_file_parser/1_intro.md +++ b/examples/21_environment_file_parser/1_intro.md @@ -2,11 +2,8 @@ *You have a problem. You decide to use generics. Now you have a `Problem where T: Clone + Send + Sync + 'static`.* -This chapter parses `.env`-style configuration files. -Two new things show up: - -1. **Splitting a string at the first occurrence of a separator.** -2. **A generic function** that works for any type the caller wants to parse into. +We'll build a parser for `.env`-style configuration files. +Along the way, you'll split a string only once and write a generic function that parses into the type its caller asks for. ## Splitting once @@ -48,7 +45,7 @@ The `where T: FromStr` clause says "T must implement the `FromStr` trait", which ## Trim and skip Real config files have empty lines, comments, and trailing whitespace. -The usual handling chain is: +A small loop handles all three cases: ```rust for line in content.lines() { @@ -65,7 +62,7 @@ Its sibling, `break`, exits the loop entirely. ## When errors mix: `Box` -This chapter's parser uses one error type, the custom `ParseError` enum you'll build below, so `?` propagates it cleanly. +The parser you're about to write uses one error type, the custom `ParseError` enum, so `?` propagates it cleanly. Real programs often mix error types: read the file from disk and you get a `std::io::Error`; parse its contents and you get your own `ParseError`. A function using `?` insists on one error type, so you need something both can turn into. @@ -87,7 +84,7 @@ For quick programs, [`anyhow`](https://docs.rs/anyhow) wraps the `Box ## A note on raw strings: `r#"..."#` -The tests in this chapter use raw string literals to embed a multi-line `.env` snippet without escaping anything: +The tests use raw string literals so you can embed a multi-line `.env` snippet without escaping anything: ```rust let content = r#" diff --git a/examples/21_environment_file_parser/2_parse_line.md b/examples/21_environment_file_parser/2_parse_line.md index de6376b..205dd8e 100644 --- a/examples/21_environment_file_parser/2_parse_line.md +++ b/examples/21_environment_file_parser/2_parse_line.md @@ -7,7 +7,7 @@ The `.env` format is `KEY=value`, but real-world files have surrounding whitespa `str::split_once('=')` is the right tool: it gives back `Option<(&str, &str)>` containing the part before and after the first `=`. From there it's `trim` plus a couple of validity checks. -We also introduce a small `ParseError` enum that the rest of the chapter will reuse. +We'll reuse the small `ParseError` enum from this page as the parser grows. ## Useful from the standard library diff --git a/examples/21_environment_file_parser/3_parse_file.md b/examples/21_environment_file_parser/3_parse_file.md index 5594b65..1c97c99 100644 --- a/examples/21_environment_file_parser/3_parse_file.md +++ b/examples/21_environment_file_parser/3_parse_file.md @@ -4,8 +4,8 @@ With single-line parsing solved, the file-level function is mostly plumbing: ite Stop at the first malformed line and return an error. Strict parsing makes configuration bugs obvious instead of silently dropping values. -Each step is self-contained, so the previous step's `parse_env_line` and `ParseError` are re-declared here with `todo!()` bodies. -Re-implement them (or paste your earlier solution) so this step compiles on its own. +To keep this page runnable on its own, `parse_env_line` and `ParseError` are re-declared here with `todo!()` bodies. +Paste or reimplement your earlier solution before building the file-level parser. ## Useful from the standard library diff --git a/examples/21_environment_file_parser/5_validate.md b/examples/21_environment_file_parser/5_validate.md index 19111cd..85e9df3 100644 --- a/examples/21_environment_file_parser/5_validate.md +++ b/examples/21_environment_file_parser/5_validate.md @@ -1,7 +1,7 @@ # Validating required variables -Most apps need *some* configuration to be present at startup: a database URL, a port, an API key. -This last helper takes a list of required keys and reports the first one that's missing. +At startup, an app often needs to verify its database URL, port, or API key before doing any work. +Your helper takes a list of required keys and reports the first one that's missing. `Iterator::find` is a good fit: scan the slice, return the first key that isn't in the map, and turn that into an `Err`. If `find` returns `None`, every required key was present and the result is `Ok(())`. diff --git a/examples/21_environment_file_parser/6_what_we_learned.md b/examples/21_environment_file_parser/6_what_we_learned.md index 4c444da..f641f46 100644 --- a/examples/21_environment_file_parser/6_what_we_learned.md +++ b/examples/21_environment_file_parser/6_what_we_learned.md @@ -6,15 +6,15 @@ You parsed structured text line by line, layered file-level handling on top, exp - `split_once(delim)` is the right tool for "key/value, split at the *first* separator". Returns `Option<(&str, &str)>` with no allocation. -- `lines()` + `trim()` + `starts_with('#')` + `is_empty()` is the standard recipe for walking a config file. +- Walking a config file combines four small operations: `lines()`, `trim()`, `starts_with('#')`, and `is_empty()`. A `for` loop with `continue` and `?` reads better than an iterator chain when the body has both kinds of control flow. -- A small custom error enum (`ParseError`) lives nicely next to the parser. +- Keeping a small custom error enum such as `ParseError` next to the parser gives the parsing code one concrete error type. `?` propagates it without ceremony as long as the function returns `Result<_, ParseError>`. - When a function mixes error types (reading the file gives `io::Error`, parsing gives `ParseError`), `?` needs one common type. `Box` is the quick catch-all almost any error converts into; a custom enum (often generated by [`thiserror`](https://docs.rs/thiserror)) keeps the type information when callers need it. - Generics let one function serve many caller types. The `where T: FromStr` bound is what makes `.parse()` work, and the caller pins `T` with a type annotation or a turbofish. -- `result.ok()` is the easy way to drop an error and produce `Option` when you genuinely don't care which kind of parse failure happened. +- Use `result.ok()` to drop an error and produce `Option` when you genuinely don't care which kind of parse failure happened. - Raw string literals (`r#"..."#`) embed multi-line text without escaping. The number of `#`s on each side just has to be enough to avoid colliding with the body. - For real apps, the [`dotenvy`](https://docs.rs/dotenvy) crate reads `.env` files into the process environment; the parser you just wrote is a stripped-down version of the same idea. diff --git a/examples/22_csv_parser/1_intro.md b/examples/22_csv_parser/1_intro.md index b57f4d9..8c8c04c 100644 --- a/examples/22_csv_parser/1_intro.md +++ b/examples/22_csv_parser/1_intro.md @@ -40,25 +40,24 @@ fn parse(line: &str) -> Vec { } ``` -Two things worth pointing out: +Each less familiar tool removes one bit of bookkeeping from the loop: - `peekable()` lets you look at the next character without consuming it. - Essential when one character's meaning depends on the one after it (the `""` -> `"` rule here). + That lookahead matters when one character's meaning depends on the one after it, as in the `""` -> `"` rule. - `match` on a tuple `(c, in_quotes)` lets you express each transition as one arm. - Easier to read than nested `if`/`else`. + The alternatives stay flatter than they would with nested `if`/`else` blocks. - [`std::mem::take`](https://doc.rust-lang.org/std/mem/fn.take.html) gives you the current string and replaces it with an empty one in a single move. - No clone, no temporary. + The old buffer moves into `fields` without a clone. ## A note on `while let` -`while let Some(c) = chars.next() { ... }` is the loop counterpart of `if let` from the Option chapter. -It keeps running as long as the pattern matches, and stops as soon as it doesn't. -Iterators return `None` at the end, so `while let Some(...)` is a natural fit when you need more control than a `for` loop gives you (here we want to call `chars.next()` again *inside* the loop body to consume the second `"`). +You used `if let` with `Option` earlier; `while let` repeats the same pattern until it stops matching. +That extra control matters here because the loop sometimes calls `chars.next()` again to consume the second `"`. -Tuple matching like `match (c, in_quotes) { ... }` is the same idea as the tuples chapter's `let (a, b) = pair`, just used as a `match` scrutinee. -The arms then pattern-match both elements at once, and the guards (the Result chapter's `if chars.peek() == Some(&'"')`) do the rest. +You also used tuple patterns in `let (a, b) = pair`; here, `match` inspects the character and quote state together. +A guard adds the lookahead check only to the escaped-quote arm. -The tests in this chapter also lean heavily on raw strings (`r#"..."#`, introduced in the env-file parser chapter) so the CSV examples can contain literal commas and quotes without an escape forest. +These tests use the raw strings you met in the env-file parser, so the CSV examples can contain literal commas and quotes without an escape forest. ## A useful tactic @@ -67,4 +66,4 @@ Then upgrade to the state-machine version for the harder cases. Failing tests give you concrete examples to think against, instead of trying to imagine every edge case up front. For real CSV in production code, reach for the [`csv` crate](https://docs.rs/csv); it handles all the corners that this exercise glosses over. -But knowing how to write a state machine yourself is a transferable skill. +Here we're after the state-machine loop, not a production-ready CSV implementation. diff --git a/examples/22_csv_parser/4_quoted_line.md b/examples/22_csv_parser/4_quoted_line.md index 29818a0..f0dc725 100644 --- a/examples/22_csv_parser/4_quoted_line.md +++ b/examples/22_csv_parser/4_quoted_line.md @@ -4,7 +4,7 @@ Real CSV is a state machine in disguise. A field can be wrapped in double quotes, in which case any commas *inside* the quotes are part of the field, not separators. And a literal `"` inside a quoted field is encoded as `""` (two quotes). -Suggested order of attack: +Let the tests lead you from one case to the next: 1. Plain `a,b,c` and simply quoted `"a","b","c"` (the basic test). 2. Commas inside quoted fields: `"a,b",c`. diff --git a/examples/22_csv_parser/5_parse_file.md b/examples/22_csv_parser/5_parse_file.md index 3513154..b9d3c7e 100644 --- a/examples/22_csv_parser/5_parse_file.md +++ b/examples/22_csv_parser/5_parse_file.md @@ -4,9 +4,9 @@ With a working line parser, the file-level parser is mostly plumbing: split on n Use [`str::lines`](https://doc.rust-lang.org/std/primitive.str.html#method.lines) to split: it handles trailing newlines gracefully, so `"a,b\n"` gives one line, not two. -This step composes on top of `parse_csv_line` from the previous step. -To keep each step independently runnable, the signature is re-declared here as a stub with `todo!()`. -Replace it with your solution from step 4 (or just call into it). +You'll reuse `parse_csv_line` from the previous page. +To keep this page independently runnable, its signature is re-declared here as a stub with `todo!()`. +Paste your earlier solution into the stub or call into it. ## Useful from the standard library diff --git a/examples/22_csv_parser/6_what_we_learned.md b/examples/22_csv_parser/6_what_we_learned.md index c174f75..1abe75f 100644 --- a/examples/22_csv_parser/6_what_we_learned.md +++ b/examples/22_csv_parser/6_what_we_learned.md @@ -1,18 +1,18 @@ # Wrapping up the CSV parser -You wrote the easy version of CSV with `split` and `trim`, then upgraded to a real state machine that handles quoted fields and escaped quotes, and glued the parsed lines into headers + rows. +You started with the easy version of CSV using `split` and `trim`. +Then you upgraded it to a state machine for quoted fields and escaped quotes before collecting the parsed lines into headers and rows. ## What we learned -- Stateful parsing comes up everywhere CSV doesn't (JSON, command lines, terminal escape sequences). - The routine is always: walk the input character by character, keep a small flag (or enum) of current state, occasionally emit a result. -- A peekable iterator is the standard tool for "what comes next?" decisions like the `""` -> `"` escape rule. - `Iterator::peekable` costs nothing in practice. +- The same stateful parsing pattern appears in JSON, command lines, and terminal escape sequences. + In each case, read one item, consult the current state, then update the state or emit a result. +- A peekable iterator lets you inspect what comes next without consuming it, as the `""` -> `"` escape rule requires. - `match (token, state) { ... }` over a tuple expresses each state transition in one line. Match guards (`if cond`) handle the cases where the transition depends on the lookahead. - `std::mem::take(&mut s)` gives you the current value and replaces it with `Default` in one move. Cleaner than clone-then-clear when you're harvesting an accumulator. - The simple `split`/`trim` version is worth writing first. It passes the easy tests and gives you a baseline; the state-machine upgrade then has concrete failing cases to react to. -- For real CSV in production, reach for the [`csv` crate](https://docs.rs/csv): it handles all the corner cases (BOMs, custom delimiters, escaped newlines inside fields) this exercise glosses over. - Writing the parser by hand once is still worth doing for the transferable state-machine technique. +- Hand production CSV files to the [`csv` crate](https://docs.rs/csv), which handles BOMs, custom delimiters, and escaped newlines inside fields. + Keep the state-machine loop for parsers you do need to write yourself. diff --git a/examples/23_smart_pointers/1_intro.md b/examples/23_smart_pointers/1_intro.md index c1a352c..b1fc472 100644 --- a/examples/23_smart_pointers/1_intro.md +++ b/examples/23_smart_pointers/1_intro.md @@ -16,20 +16,21 @@ A `String` in Java is a reference to a heap object that the garbage collector re Those references are not smart pointers in the Rust sense: there is no single *owner*, and you don't know when (or whether) cleanup happens. Rust's smart pointers give you the heap allocation without the GC, because ownership tells the compiler exactly when to drop. -## What's in this chapter +## Where `Box` earns its keep 1. **`Box`: heap allocation with a single owner.** The simplest smart pointer. You give it a value, it puts it on the heap, and it frees it when the box goes out of scope. 2. **Recursive types.** Some types are impossible to write without indirection. A linked-list node that contains *another* node would be infinitely sized; `Box` gives the compiler a fixed-size handle to put in the struct. -3. **`Box`.** Picking up where the traits chapter left off. +3. **`Box`.** + You met `dyn Trait` earlier; now you'll give the trait object an owner. A trait object like `dyn Shape` doesn't have a known size, so it has to live behind a pointer. `Box` is the owned form, and it's what lets you store a `Vec` of mixed concrete types that all implement the same trait. - The env-file parser chapter uses the same trick with `Box`. + You saw the same trick with `Box` in the env-file parser. -## Two more smart pointers worth knowing about +## Recognizing `Rc` and `RefCell` -Real codebases reach for two other smart pointers often enough that they deserve a mention here, even though the exercises in this chapter focus on `Box`. +The exercises focus on `Box`, but `Rc` and `RefCell` appear often enough in Rust code that you should know what their names promise. ### `Rc`: shared ownership, single-threaded @@ -47,7 +48,7 @@ let c = Rc::clone(&a); // count is now 3 ``` `Rc` is single-threaded. -The multi-threaded equivalent is `Arc` ("atomically reference counted"), which you'll meet when concurrency shows up. +`Arc` ("atomically reference counted") provides shared ownership across threads, as long as the value inside is itself safe to share between threads. C++ devs: `Rc` is `shared_ptr` without the atomic overhead, `Arc` is `shared_ptr` with it. ### `RefCell`: interior mutability @@ -59,4 +60,4 @@ If the borrowing rules are violated, the program panics instead of failing to co If you're coming from Java, this is close to a field with a private setter: outside code holds an immutable handle to the object, but the object can still mutate itself. You will not need `RefCell` for a long time. It pairs with `Rc` to build graph-shaped data, and it shows up in some testing patterns. -Mentioned here so you recognize the name when you see it. +For now, recognizing the name is enough. diff --git a/examples/23_smart_pointers/4_pipeline.md b/examples/23_smart_pointers/4_pipeline.md index 1f67859..411e684 100644 --- a/examples/23_smart_pointers/4_pipeline.md +++ b/examples/23_smart_pointers/4_pipeline.md @@ -1,6 +1,6 @@ # Mixed types behind one trait: `Box` -The traits chapter introduced trait objects (`dyn Trait`) and ended on a puzzle: a `dyn Trait` doesn't have a known size at compile time (different implementors are different sizes), so the compiler won't let you put one directly in a `Vec` or return one from a function. +When we worked with traits, `dyn Trait` left us with one puzzle: different implementors have different sizes, so the compiler won't let you put a trait object directly in a `Vec` or return one from a function. The fix is to put it behind a pointer, and the *owned* pointer is `Box`. ```rust @@ -13,15 +13,15 @@ let pipeline: Vec> = vec![ Every entry in the vector is one box, one pointer wide, all the same size. Each box owns whatever concrete type it wraps. Dropping the vector drops the boxes, which drops the inner values. -This is the exact same pattern the env-file parser chapter uses as `Box`: "some value, I don't care which concrete type, just give me one owned thing that implements the trait." +You've already seen the same pattern as `Box` in the env-file parser: "some value, I don't care which concrete type, just give me one owned thing that implements the trait." Calling a method on a `Box` looks like calling it on the concrete type: `cmd.run(input)`. Under the hood, Rust does a *vtable lookup* (the same trick C++ uses for virtual methods) to pick the right implementation. -The cost is one extra indirection per call; the benefit is the heterogeneity above. +You pay one extra indirection per call in exchange for storing different concrete types in one vector. ## What you're building -A tiny text-transformation pipeline. +You'll build a tiny text-transformation pipeline. The trait is one method: ```rust @@ -36,11 +36,10 @@ Three commands are already implemented for you: - `Reverse` reverses the input. - `Append { suffix }` appends a configured suffix. -The exercise is the orchestrator: `apply_pipeline` threads an input string through every command in order, feeding each command's output into the next command's input, and returns the final result. +Your job is the orchestration: `apply_pipeline` threads an input string through every command in order, feeding each command's output into the next command's input, and returns the final result. An empty pipeline returns the input unchanged. -The reason this works is `Box`. -The pipeline can mix `Uppercase` (a unit struct), `Reverse` (also a unit struct), and `Append { suffix: String }` (carries a field) in the same `Vec`, because each one is hidden behind the same fat pointer. +Because each command sits behind `Box`, the same `Vec` can hold `Uppercase`, `Reverse`, and `Append { suffix: String }` even though their concrete types have different sizes. A generic `Vec` where `C: Command` would only let you pick *one* concrete command type per pipeline. ## Useful from the standard library diff --git a/examples/23_smart_pointers/5_hints.md b/examples/23_smart_pointers/5_hints.md index 415782e..03a8d91 100644 --- a/examples/23_smart_pointers/5_hints.md +++ b/examples/23_smart_pointers/5_hints.md @@ -22,4 +22,4 @@ That gets the empty-pipeline test for free. 3. Method calls go through the box automatically. `cmd.run(...)` is the only thing you call inside the loop. -4. Once you reach the iterators chapter, this same loop collapses into a single `.fold()` over `commands`. +4. The same loop can also be written as the `.fold()` you met in the iterators chapter. diff --git a/examples/23_smart_pointers/6_what_we_learned.md b/examples/23_smart_pointers/6_what_we_learned.md index a028633..d36b7c9 100644 --- a/examples/23_smart_pointers/6_what_we_learned.md +++ b/examples/23_smart_pointers/6_what_we_learned.md @@ -1,6 +1,7 @@ # Wrapping up smart pointers -You boxed an integer and added it back out, defined a recursive expression-tree type that only compiles because of `Box`, and threaded an input string through a heterogeneous pipeline of text-transformation commands via `Box`. +You boxed an integer and added it back out, then used `Box` to make a recursive expression tree possible. +Finally, you threaded a string through different command types stored behind `Box`. ## What we learned @@ -15,9 +16,9 @@ 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 from the env-file parser chapter. + It lets one vector own different concrete types behind a shared interface, just as `Box` held different error types in the env-file parser. - Dynamic dispatch through a trait object costs one vtable lookup per call. - That's usually fine. + For this small command pipeline, one lookup per stage is unlikely to matter. Reach for generics (`fn f`) when you want the compiler to monomorphize away the indirection. ## Other smart pointers, briefly @@ -25,13 +26,13 @@ You boxed an integer and added it back out, defined a recursive expression-tree - `Rc` ("reference counted") gives you multiple owners on a single thread. The value is dropped when the last `Rc` goes away. C++ analogue: `std::shared_ptr` without the atomic overhead. -- `Arc` is the same idea but safe to share across threads. - It shows up when concurrency does. +- `Arc` uses atomic reference counting for shared ownership across threads. + The value inside still has to be safe to share between threads. - `RefCell` provides *interior mutability*: borrow checking moves from compile time to runtime, so you can mutate through a shared reference. It pairs with `Rc` for graph-shaped data and shows up in some testing patterns. You can go a long way without needing it. -## Where this goes next +## Connections to earlier chapters -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 env-file parser chapter brings `Box` and the `?` operator together, which is the day-to-day payoff for understanding `Box` here. +The explicit loop in `apply_pipeline` has the same shape as `.fold(...)`: each command receives the previous output and produces the next one. +`Box` in the env-file parser uses the same owned trait-object pattern to hold different error types. diff --git a/examples/24_rust_fundamentals_quiz/1_intro.md b/examples/24_rust_fundamentals_quiz/1_intro.md index 84e490d..ee4e99a 100644 --- a/examples/24_rust_fundamentals_quiz/1_intro.md +++ b/examples/24_rust_fundamentals_quiz/1_intro.md @@ -3,7 +3,8 @@ *If you can answer these without flipping back, you've internalised more Rust than you think.* Twenty multiple-choice questions covering the ground we've walked together: ownership and borrowing, `Option` and `Result`, lifetimes, traits, enums, modules. -Pick an answer to lock it in and reveal the explanations, not just for the right answer, but for every distractor too, because the wrong answers are usually where the learning lives. +Pick an answer to lock it in and reveal the explanation for every choice, not just the right one. +The distractor explanations show why the plausible alternatives do not fit. > [!TIP] > No grade, no time limit, no record kept. diff --git a/examples/25_appendix/1_intro.md b/examples/25_appendix/1_intro.md index 91a4805..90b2379 100644 --- a/examples/25_appendix/1_intro.md +++ b/examples/25_appendix/1_intro.md @@ -1,4 +1,4 @@ # Appendix -> This is an appendix to the course. -> It contains additional material that may or may not be interesting to you. +> The course ends here, but a few side paths remain if you're curious. +> You can read how it came together and why the exercises are arranged this way. diff --git a/examples/25_appendix/2_about.md b/examples/25_appendix/2_about.md index 19f7e9a..4618ea4 100644 --- a/examples/25_appendix/2_about.md +++ b/examples/25_appendix/2_about.md @@ -19,27 +19,26 @@ This site is that path, with an editor attached. ## How to keep going -The course covers the core mechanics. -Real fluency comes from using those mechanics on problems that matter to you. -A few suggestions: +You've covered the core mechanics. +Now give them a problem that matters to you: - **Build something tiny.** A CLI that does one thing for you. A scraper. A toy interpreter. The smaller and more personal, the better. Finishing it teaches more than reading another tutorial. -- **Read other people's Rust.** Pick a small crate you find useful and read its source. - The Rust ecosystem leans toward clean, idiomatic code; you'll absorb a lot just by skimming. +- **Read other people's Rust.** + Pick a small crate you use and trace one path through its source, from a public function to its return value. + You do not need to understand the whole crate to pick up an idiom or two. - **Use it for the next thing you'd otherwise do in another language.** The first project will be slow. - The second will be noticeably less slow. - By the fifth, Rust feels like home. -- **Stay curious.** Rust is a deep language; nobody knows all of it. - When something surprises you, that's the language inviting you to dig in. + On the next one, you'll already have decisions to reuse for errors, modules, and tests. +- **Follow surprises.** + When behavior or a compiler message catches you off guard, reduce it to a small example and change one thing at a time. ## A note from corrode This course is open source on [github.com/corrode/course](https://github.com/corrode/course). -Issues and pull requests welcome. +Issues and pull requests are welcome. If you'd like Rust training, code review, or consulting for your team, see [corrode.dev](https://corrode.dev). Thanks for spending time here. diff --git a/examples/25_appendix/3_design.md b/examples/25_appendix/3_design.md index 1ac0469..4563df3 100644 --- a/examples/25_appendix/3_design.md +++ b/examples/25_appendix/3_design.md @@ -1,30 +1,29 @@ # Why this course is built the way it is -A few deliberate choices set this course apart from reading the official book front to back. -None of them are knocks on the book, which is excellent. -They're different bets about how people actually pick up a language. +I built this course around a simple bet: Rust sinks in faster when you write code before you study every rule. +The choices below follow from that bet. -| The usual approach | What this course does | +| A reading-first path | This course | |---|---| | Read several chapters before writing any code | You write code from the first exercise | | Ownership dumped as theory early on | Ownership introduced as a spiral, consolidated late | | Little "why should I care?" motivation | Problem-first: show the bug Rust prevents, then the fix | | No feedback when your code is wrong | Tests and focused `todo!()` stubs that point at what's missing | -The throughline is that you learn Rust by hitting its rules in practice, not by reading about them in advance. -The borrow checker makes more sense after you've watched it reject something than it does as a list of rules up front, so the course leans on that order everywhere. +Here you learn Rust by hitting its rules in practice, not by reading about them in advance. +The borrow checker makes more sense after you've watched it reject something than it does as a list of rules up front, so we use that order throughout. ## Future directions -Some ideas didn't make this version but are worth writing down so they aren't lost: +I haven't built all the ideas below, but I don't want to lose them: - Difficulty signposting: an explicit "this chapter is a cliff" warning before the hard spots. - A dedicated closures chapter (`Fn`, `FnMut`, `FnOnce`, and capture semantics) as another turn of the ownership spiral. - A fearless-concurrency chapter, the fourth pillar alongside no null, no exceptions, and memory safety without a GC. - A problem-first language picker: ask which language you're coming from and show the bug in that language first. - For now the prose stays language-neutral. + For now, I keep the prose language-neutral. - Pillar slogan titles for the chapters ("No null", "No exceptions", and so on). - For now that motivation lives in each chapter's opening lines. + For now, I keep that motivation in the opening lines. - A running project thread that carries one program across chapters instead of standalone exercises. If you have opinions on any of these, the issue tracker is open.