Feedback Requested: The Rust Book Abridged

I'm really happy that there is (no joke) yet another explanation about lifetime annotations.

By far the fundamental topic I had the hardest time with. I remember loathing the long and mostly un-rewarding reads. I gave your version a read and it seem like a good representation of all the things that clicked for me.

I will say that instead of saying "It's important to note" at the end of a paragraph, it would be better to give "annotations don't actually change the lifetime..." some sort of formatting emphasis, similar to the Info/Tip cards.

Here's some notes I took as I skimmed it. I didn't cross-check these against The Book so it's possible analogous improvements could be made there.

Collapsed notes
  • 2 - Programming a Guessing Game | The rs Book

    C++ references are pretty different from Rust references, which throws C++ newcomers from time to time.

    Covering the syntax and SemVer is probably worth it, but maybe mention cargo add too.

  • 3 - Common Programming Concepts | The rs Book

    Not at all specific to this book but I'm always a little annoyed by reading this and then having to backtrack later when shared/interior mutability comes up. mut bindings are primarily a lint while exclusive references (&mut) are very distinct from anything else in the language. Not binding with mut means you can't take an exclusive reference (&mut) to the variable or overwrite the variable. Whether or not you can mutate it depends on the type.

    Maybe at least have a heads-up about shared mutability linking to some later chapter.

    I don't know that declaring statics is all that rare. Promoted statics (e.g. literal strings) abound.

    Overflow checks are independent of the profile, though you accurately describe the profile defaults.

    It's a unicode scalar value.

    No, it's not. It's an unsized type, aka a dynamically sized type (aka a DST). The length is not known at compile time and strs of any length have the same type (unlike arrays, where the size is a parameter of the type).

    I think I see what you're trying to say, but you can put one on the heap like any other type, no problem. "The data within an array is stored inline, so a variable with an array type will be on the stack"?

    Nit: that's always the case and it's more like, if the type is () the if block can be a statement.

  • 4 - Ownership, References, and Slices | The rs Book

    Rc and Arc (which are not part of the language) provide what is typically called shared ownership. You probably cover these later; perhaps link ahead with a side-bar.

    statics don't really have owners either. Nor do leaked values.


    The heap/stack distinctions are a bit weird in this chapter. Things on the stack that don't involve allocations also get destroyed when they go out of scope. You can also transfer ownership of things on the stack which aren't associated with allocations on the heap.

    The "only one &mut" is a simplification in the face of reborrows, but an extremely common one.

  • 5 - Using Structs to Structure Related Data | The rs Book

    You can move username and then access user1.email. It's known as a partial move.

    The bouncing between self: &self and &self or self or &mut self nomenclature is somewhat confusing. Especially the last part, which also conflates mut bindings with &mut references.

    • If I want to modify self (self: Self), I need mut self because self is not a mut binding by default (like any other parameter).
    • If I need to modify through a reference (and don't have shared mutability), I need &mut self instead of &self. Function parameters are not shared reference, or references at all, by default.
    • If I do use &mut self (self: &mut Self), The self binding is not mutable. That would be mut self: &mut Self.
  • 7 - Managing Growing Projects with Packages, Crates, and Modules | The rs Book

    Is it frowned upon? I think it's more a matter of preference. There's clippy lints to enforce either style.

    Perhaps note that you rarely want to import * if it's not a prelude (or perhaps super or other niche cases).

  • 8 - Common Collections | The rs Book

    Unicode terminology is a mess and Rust is no exception, but having these two descriptions adjacent to one another is pretty jarring to me. It's a vector of UTF8 bytes.

    It doesn't store char and Indexing works in-place, so it can't return a char. (It could return a &str covering the first char-equivalent).

    s.bytes() and s.chars() operate on str (and String via deref coercion), and return iterators, not arrays (or Vecs). String::into_bytes is what converts a String into its underlying bytes. str also has as_bytes.

    You could mention BTreeMap.

    The exact algorithm used by default is an implementation detail. I thought there were some baseline guarantees documented somewhere, but I didn't see them off hand just now.

  • 9 - Error Handling | The rs Book

    And unimplemented! (and unreachable!, etc).

    I skimmed this portion pretty fast, but I'm not sure if you covered that ? on a Result will try to convert the Err variant with .into() (e.g. I'm pretty sure your match didn't). That is to say, I'm not sure you defined "compatible error type".

    You side-barred catching panics, :+1:, but a thread can also panic without halting the whole program.

I'll probably skim the rest later.

7 Likes

That's a good idea! I'll move that bit into a card.

I was also thinking it might be good to do an example in the lifetime annotation on structs that point out that:

{
let hello = String::from("hello");

    {
        let s = MyStruct { value: &hello }
    }

}

Here the 'a in MyStruct<'a> is as long as the life of hello, and not as long as the life of MyStruct. Do you think that's a clear example?

Wow! Thanks so much for the detailed read through!

How come I didn't know about cargo add -_-. You'd think I'd have tried it - it's the same command as npm.

Go doesn't have references either - go has pointers, which behave just like Rust "references". Good catch!

Good call. I'll point out that strings are immutable when not mut, but this isn't true for all objects. Mutex is maybe a good example of an object that's immutable, but you're allowed to change the contents if you own the lock.

Oh... I suppose that's true. I think of &str most often as pointing to a string literal, but I suppose they can be a view into any string.

It's 1AM here so I'll read the rest of this tomorrow! But thanks again!

I'd say they don't - they're more like Rust Arcs, in that they are keeping the pointed-to value alive. Rust references very explicitly (and very importantly) don't.

4 Likes

It was only stabilized recently, to be fair.

I'm going to say not clear, since I was almost finished writing about how I didn't understand the point, when I realized what it was trying to say :sweat_smile:.

It might make more sense to just say that structs always need lifetime annotations (no elision like in functions) and need each of its references to outlive itself. Or at least add a counterexample where the reference does not live long enough and results in an error.

I made a few changes to the first half of chapter 4 - you were right about leaning too much on the heap side of things. I also added a side bar about how, when moving ownership of a variable, the stack-owned part of the variable will change its location in memory.

statics don't really have owners either. Nor do leaked values.

statics like static variables? Would you not say a String in a static variable is owned by the variable? Since that variable will never go out of scope, and you can't move out of a static the String will never be dropped, it's maybe just a question of semantics. Or is there some specific reason why you say statics don't have an owner?

Leaked values is an interesting side-bar. :slight_smile:

The exact algorithm used by default is an implementation detail. I thought there were some baseline guarantees documented somewhere, but I didn't see them off hand just now.

If you go look at the DefaultHasher implementation, it just calls into SipHasher13. But the fact that it's unspecified is important. I'll add something like "at the time of this writing".

And unimplemented! (and unreachable!, etc).

I'm learning quite a bit just from your comments! :slight_smile: Thanks again for taking the time to do this.

I'm not sure if you covered that ? on a Result will try to convert the Err variant with .into()

There is a tip box about that just above. Although, I'm going to add a section to the chapter about traits on From and Into, because the first time I saw someone call foo.into() it was a little surprising.

Semantics I suppose, but sensible ones. You can't move them, you can't drop them, you can't assume there's no outstanding references to them; from a compiler perspective, they're equally visible and accessible from everywhere in the code simultaneously, unlike a normal variable that can be reasoned about locally.

I think it makes sense to consider a static variable a degenerate case of an owner, in basically the same way that &'static is a degenerate case of a borrow.

  • A &'static reference is a reference, which never becomes unusable because the thing it borrows exists forever.
  • A static variable is a variable, whose value is never dropped or moved because the scope it is in exists forever.

In both cases, the staticness is constraining when certain things can happen to “never”, but they're exactly the same as their non-static forms otherwise.

But — ownership isn't exactly a concrete thing that can be explicitly identified in Rust; rather, it is a pattern that code cooperates to maintain. It's equally valid to not count statics as owners, because they occupy the degenerate case where both models fit. 1 × 0 = 2 × 0.

1 Like

Round 2 :slight_smile:

Collapsed notes
  • 10.2 - Traits: Defining Shared Behavior | The rs Book

    "need to implement"
    You can only pick and choose things with defaults; a complete implementation must result.

    This is an oversimplification, but another extremely common one. Your orphan rule example is great.

    Conflating traits with types, and eventually understanding trait objects, is a common point of confusion. I'd suggest not phrasing this as "use a trait as a type".

    "using a where clause"

    "Using impl Trait as a return type"

    Since you present argument-position impl Trait (APIT) and return-position impl Trait (RPIT) right next to each other, you should take a moment to explicitly point out that they act very differently despite being spelled the same. This confuses a lot of people and conflating the two with the same syntax was arguably a misstep in language design.

    Another use case for RPIT is to return unnameable types, like closures... for example iterators that use map. And another is to be lazily future compatible by giving yourself the ability to change exactly what is returned without hiding it behind a new local type. (A downside to your consumers is that now they can't name the type.)

  • 10.3 - Validating References with Lifetimes | The rs Book

    Variables like x have liveness and drop scopes, but they don't have lifetimes, and I feel that conflating the two causes more confusion than it helps build accurate mental models. Consider this code:

    fn main() {
        {
            let a: &'static str = "";
        }
    }
    

    a has a liveness scope that ends at the inner block, but the type of a includes a lifetime which is 'static. For those who have conflated liveness scope and "the lifetime" of something, this can cause a lot of confusion, especially as examples become more complicated (like in this other running thread I'm participating in).

    You can't borrow x for a lifetime greater than x's liveness scope, but the only actual lifetime involved here is that of the borrow. There is no 'b lifetime in the analysis.

    The signature does in fact force you to pass in two references with lifetimes that are the same.[1] I believe what you're getting at is that a caller could have a couple references with different lifetimes in variables, and pass those in to the function in the example. That's true, but they're actually passing in copies or reborrows of those references which have been coerced to the same lifetime.

    In other words, you're conflating the variables (or their lifetimes/types) used at the call site with x and y (or their lifetimes/types). Thanks to subtyping and reborrowing, they aren't the same.

    The fact that giving lifetimes the same name forces them to be the same is pretty important (e.g. when trying to understand borrow errors involving invariance). The way to understand why you can pass in variables with different lifetimes in this case is to get a feel for the variance of references (the ability to coerce their outer lifetimes to shorter lifetimes).

    That's incorrect; the conflation between x and y and the call site really comes through here.

    • it will be exactly the lifetime of x and y, which are the same;
    • that will be at most the shorter of the two references at the call site, but could also be shorter than both

    Functions can impose lifetime constraints which, for example, require lifetimes to be the same at the call site. Here's your example with invariant lifetimes.

    Seems like a good time to point out the function lifetime elision rules. ... ok, I see it in the TOC side bar, so probably just add a note that you'll talk about defaults that can be elided later.

    This is another example of conflating a liveness scope with a lifetime.

    The wording is a little weird. If it was possible to omit annotating the struct, the language would either give every elided lifetime the same lifetime or distinct lifetimes; if Rust went with the former this would fail, and it if it went with the latter it would succeed. (Personally I strongly hope we can never elide so much that which of the two is happening is unclear.)

    "lifetime". You use "lifecycle" at least one other time.

  • 12 - An I/O Project: Building a Command Line Program | The rs Book

    Somewhat case by case in my experience. More common when the parent module has a short name.

    Great job pointing out args_os, almost no tutorials do that. [2]

    That's a really weird example. I'm supposed to call args myself, collect it in a Vec presumably, then give you a slice, and you're going to ignore the first element of the slice and clone things out of the rest? It's like a "concrete abstraction", but instead a "concrete refactor" (that forces inefficencies).

    ...oh, you agree.

    Though as a tutorial consumer, at this point, I'm still like ... so why are we doing it?

    It seems that no one would actually write this; the only reason to write this is to have something to improve later on in the tutorial. (If it's just "let's practice writing code" that's fine I guess, but it's presented as something reasonable to write in a practical setting.)

  • 13 - Functional Language Features: Iterators and Closures | The rs Book

    It can be higher-ranked over lifetimes (but that's perhaps too confusing to point that out here).

    Well, it's more general than that.

    (This is more of a code review comment, but anyway.) So practically speaking, there's still only really one way to hold this method, and that's by passing it something based on args() or (small chance) args_os(). If you're going to make that assumption, just use env::args_os within build. Then callers don't have to pass anything at all, and you can handle non-unicode in a non-panicking way since build is fallible.

  • 15 - Smart Pointers | The rs Book

    It does have a whole bunch of special behavior though.

    This is conflating &mut and mut bindings again. y doesn't need to be a mut binding, it needs to be an entirely different type (&mut).

    Perhaps not a good place to point this out to readers, but one of Box's special behavior is that you can move (even non-Copy) things out of a Box with *.

    If you want a reference to the contents, you need either &* or just & in a place with deref coersion. * only works if you also have auto-ref.

    No, but they can take a &Box<i32>. In your example,

    • That's not a function, it's a macro, which can do all sorts of whacky stuff
      • and might expand to something with == which can also auto reference
    • If it were a function, you'd be passing two i32s, not &i32 and a Box<i32>
    • Neither work with an actual function since those don't have auto-ref
    • Here's some that do work, though your deref coercion comments only make sense when you don't use *

    The Drop trait is used for all sorts of things. Did you mean that when implementing a smart pointer, one usually needs to implement Drop? You emphasized Drop earlier somewhere too and I'm not sure why.

    Hard no, it is undefined behavior and it is incorrect. The language defines what is UB, and mutating through & is UB (run Miri under Tools in the top right). It would also be easy to modify this to demonstrate a data race.

    Also, UB results in something more nefarious than a panic. UB can result in anything. If it happens to result in a panic, you're lucky. We'd have a lot less security problems and find bugs a lot quicker if UB usually panicked instead of continuing on in a corrupt state, etc.

    unsafe shouldn't be used if one doesn't understand what must be upheld.

    No, no. RefCell's implemention does use unsafe but it's not exactly what you described, and saying that it is gives a very dangerous wrong impression. UnsafeCell is the language-level construct that makes interior mutability sound. Interior mutability is not "I threw it in unsafe because I (thought I) knew better."

    No, it stores it inline.

    You can have a mutable binding of a RefCell<T>, and create a &mut to one too; it's not inherently immutable. The key capability is that you can call borrow and borrow_mut with &RefCell<T>.

  • 16 - Fearless Concurrency | The rs Book
    Between 16.1 and 16.2 seems like a good place to cover thread::scope. And to point out its differences, like it does join all the threads.

    You still get a lock, but it's poisoned. You can still use a poisoned Mutex. But it's true the most common thing to do is just unwrap and propagate more panics.

  • 17 - Object Oriented Features of Rust | The rs Book

    Probably would give the wrong impression to point it out here, but sometimes dynamic dispatch can be devirtualized.


    I'm not sure how best to bring it up, but a common misunderstanding is to think that dyn Traits are dynamically typed, and not themselves a concrete type.

  • 18 - Patterns and Matching | The rs Book
    In Matching Named Variables you could point out the speedbump of mistyping a variant name and having it treated as a new variable binding.

    Seems like a decent chapter to mention matches!.

  • 19.1 - Unsafe Rust | The rs Book

    "find"

    You lose so many guardrails I think this is a little too reassuring personally. Though it sorta depends on the unsafe too.

    Miri (under Tools, top-right) flags that example as UB. Your &mut invalidated your r1.

    You should cover Miri in this chapter. You should cover other best practices too, like always documenting proof that your unsafe is sound.

    Even single-threaded, getting two live &mut is UB, for example twice in the same call-stack can easily be UB (run Miri).

  • 19.3 - Advanced Types | The rs Book

    Or on the stack -- you don't know where the data is stored.

  • 19.4 - Advanced Functions and Closures | The rs Book

    Closures have concrete types (albeit unnameable).

    It's somewhat subtle but you're not using a function pointer there either. Functions have their own (unnameable, zero-sized) types. Same with the enum examples. (And tuple constructors aren't unique to enum variants.)


    You could mention in this section that closures which do not capture anything can be coerced to a function pointer.


  1. It's technically true that "the same" only refers to forward control flow, but this is a nuanced point and not what you were getting at, I think. ↩︎

  2. Including official ones :person_facepalming: ↩︎

4 Likes

I completely agree. There certainly won't be a one-size-fits all approach to learning Rust for newcomers, especially people who might be relatively new to programming. Good work with the book.

Some quick notes while scanning the book:
1.1 Installation in distributions should probably be managed by the package manager?
3.2 Maybe show example of how to multiply float by integer to distinguish from C/C++. That is something (i feel) that new users often do not understand initially
3.3 Maybe change comments above function to documentation?
Overall I feel that your examples are well put together and can be understood easily. Very nice.
I feel like section 8 and 9 should come before section 7, since growing projects is something that a new user will not worry about initially, but common collections will be needed from the very beginning.
16 I like that you try to relate Rust concepts to other languages features.
I actually had some comments on section 15 and 17 as well but after looking at the structure of your document I decided to omit them.

Given the vast amount of serious inaccuracies having been pointed out so far (which I need not repeat here), I would consider it a good idea to only take up such a job as writing a programming language tutorial when you have actually acquired measurable experience with the language. I understand that you have experience with other languages, but I would strongly encourage you not to propagate myths (such as cells heap-allocating or "unsafe" meaning that you are allowed to do anything) that harm the reputation of the language and confuse beginners, leading them to eg. write code with UB or optimize prematurely.

Rust is in a different enough family that someone coming from a classical OOP language might have false expectations or first impressions of its behavior. I think publishing a language guide is a big enough responsibility that it should be subject to higher-than-usual standards. I fully see and appreciate the helping intention here, but coming from academia (where misguided teaching of bad practices is, unfortunately, pervasive practice), I have serious concerns about releasing a work like this to people who aren't, pretty much by definition, by themselves able to winnow the presented advice and decide what's right and wrong in it. Sharing your experience and knowledge acquired so far is an honorable and noble act, but you have to be very careful as to not cause more harm than good by accident.

6 Likes

5 posts were split to a new topic: Scopes and lifetimes

2 posts were merged into an existing topic: Scopes and lifetimes

Notes

That's a really weird example. I'm supposed to call args myself, collect it in a Vec presumably, then give you a slice, and you're going to ignore the first element of the slice and clone things out of the rest?

Yeah... This is one of the downsides of trying to stick with the examples in the original book. :confused: I cut out a lot of the "we'll write this function this way and then refactor it to this other way, and then again!" but here it was cutting across different chapters so I left it.

This constructor isn't totally without merit. There are times where I've used some library that had an associated CLI, and the CLI arguments and the API for the library were very different. Sometimes it's nice to be able to just pass in command line arguments and let the library work it out... But even then I wouldn't want to pass in a fake command name for the library to ignore.

It's really tempting to change that example.

(Re: unsafe incrementing a counter) Hard no, it is undefined behavior and it is incorrect.

Ahh... hmmm... You read something very different into this example than I was trying to convey. I'll rework this section, for sure, because if you read that then someone else will too. But I was trying to suggest that you could "use unsafe code to solve this" indirectly by using a Cell or a RefCell. Keep in mind at this point in the book a Rust newbie is unaware that raw C-style pointers are even a thing in Rust. :stuck_out_tongue: I've rewritten this section to be a little less chatty about what unsafe means exactly since we cover that later anyways, and introduce UnsafeCell, Cell, and RefCell and explain their relationship.

You still get a lock, but it's poisoned.

This seemed like more detail than this chapter needed... But you know what? This is supposed to be a book for programmers - I'll put it in there.

No, but they can take a &Box<i32>.

You, sir or m'am, are amazing at proof reading. Thanks so much.

The Drop trait is used for all sorts of things. Did you mean that when implementing a smart pointer, one usually needs to implement Drop? You emphasized Drop earlier somewhere too and I'm not sure why.

I'll remove that. (To be honest, if I weren't trying to adhere to the structure of the original book, I'd move this whole section into the section on traits. It feels weird to introduce the Drop trait in the middle of a chapter that's supposed to be about smart pointers. Maybe I should still move it and then reference it from here...)

Miri (under Tools, top-right) flags that example as UB. Your &mut invalidated your r1.

Interesting. Stole this example straight out of the original book. There's an open issue for it.

It's somewhat subtle but you're not using a function pointer there either. Functions have their own (unnameable, zero-sized) types. Same with the enum examples. (And tuple constructors aren't unique to enum variants.)

Hmm... The original Rust Book here says: "We can use these initializer functions as function pointers that implement the closure traits, which means we can specify the initializer functions as arguments for methods that take closures", so it seems like they're conflating functions and function pointers in the same way. Do you have a link to further reading about this? I'm genuinely curious, and a search for function type rust is, as you can imagine, shockingly unhelpful. :stuck_out_tongue:

Your concerns are valid, but I also think that there is a need for tutorials from the perspective of someone who started learning rust not too long ago. It is really a question of having a different view on things, that someone with too much knowledge of the language will have long forgotten.

In some schools or universities teachers ask students who go on internships for their first time, to write an "astonishment report", a candid look at how work goes in the company and what was surprising. It is not uncommon that not only the intern but also people from the company learn a lot from it.

So in conclusion, I think that it is very promising work, and once it has received all the due feedback that it deserves to raise it to rust standards with no more errors and misguidance, this tutorial can certainly reach the status of a great tutorial!

Sounds like a lot of my notes could be applied to The Book, heh. (I haven't read it completely in some time.)

I agree with that, and have made my share of similar abstractions -- that don't expect a fake command for the literal or read-from-file versions. I guess it's a presentation problem more than anything (I think a typical program arrives at the generic version after starting with the args-only version, not after starting with some method that can only be held one way). Maybe it could be...

  • Take a Vec<String> (that doesn't include the command name)
    // usage
    let args = env::args().skip(1).collect();
    // Or your own custom args!
    let args = vec!["foo".into(), "bar".into()];
    build(args); // Or builder.build(args) or whatever I didn't relook it up
    
  • "Yeah inefficient and clunky, but don't worry. We'll improve it later"
  • fn build<I: IntoIterator<Item = String>>(iter: I)
    build(env::args().skip(1));
    build(["foo".into(), "bar".into]);
    
  • fn build<S: Into<String>, I: IntoIterator<Item = S>>(iter: I)
    build(env::args().skip(1));
    build(["foo", "bar"]);
    

Though this still loses out on handling non-unicode gracefully.

You could use those without unsafe, but it would make String not Sync. You could "work around" that with new types and unsafe, but it would then be unsound again -- they are !Sync for a reason after all (and it's to protect you from data races, not to make your life harder arbitrarily). There is a safe and sound way to do it: atomics.

I think the higher level point is that when we're talking about Rust's memory safety model -- so things like aliasing and data races -- it is impossible for the target audience to "know better" or "be smarter than the compiler"; it's almost guaranteed that if they attempt unsafe for these uses, they will get it incorrect. So one shouldn't imply that it's possible for them to know better than the compiler without putting in the work to understand what Rust considers UB.

Especially if they're coming from C/C++, they might think they know better already. I certainly did, when I started with Rust. But (a) Rust's invariants are different from Cs, and (b) as it turns out the C ecosystem is a lot more tolerant of things like "the language says it's UB but the compiler let's you get away with it". See also this recent thread. It might be nice to point this out to the audience.

It'd be even nicer to give actual guidance, but probably hard to do so. I wish I knew of a good "how to write sound unsafe code for beginners" guide, but I don't offhand. Sounds like a good future project for the OpSem team.

There are some relatively innocuous uses of unsafe -- "hey I just want to get_unchecked in my hot loop" -- but the memory safety bits are quite challenging I feel. (Too challenging, the language or tooling has room to grow here.) The invariants are not even fully decided, to boot.

Here's the page about it in the reference.

2 Likes

I like the style and it reads quickly indeed, the target audience really corresponds to what is advertised !

Some comments

  • tooling: when coming to a new language a common need for someone who has experience with other languages is to familiarise with tooling, to be more at home and productive. You introduced rustc, cargo, cargo add, clippy. I would suggest also rustfmt and rust_analyzer at some point (sorry if they are present in other pages that I did not yet read)
  • chapter 2 : just after you introduce the cargo toml it would be a good idea to add a link to the cargo book. By the way since you have a link that explains toml I think there is no need to explain what toml is. And for python people you could mention cargo being like distutils + virtualenv + pip + pip_tools + many more... but on steroids.
  • 4: a seasoned programmer would usually skim quickly through a book and dive in to the language. Sinnce this chapter is very fundamental, here it would be nice to heavily suggest reading carefully this chapter especially if coming from a GC language
  • 4.1 the mention of leaking memory at this point seems confusing
  • ownership: nice analogy of owning a book!
  • 4.2 the rules of references: how about adding an info block here to present the arguably more correct way to think about references, i.e. &x for shared references to x, and &mut for an exclusive reference to x?

In any of these languages, a trait is very similar to an interface.

Traits gave already been mentioned in one of the previous chapters, but for people who don't remember 100% or who jumped to chapter 5, I would suggest to make this sentence into a separate paragraph, something like "In rust, structs can implement Traits, which are very much like interfaces in other languages." (Oh, after reading the whole page this is not so important after all.)

  • 5.1 Creating Instances from Other Instances with Struct Update Syntax: here I would suggest to rephrase "copy" with something else like "get" or "use", so as not to confuse the reader. I feel that it is very important at this stage to precisely use copy/clone/borrow/move etc in order to reinforce the not-yet-fully-crystallized reader's learning of rust vocabulary.

  • 6.2

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(province) => {
            println!("Quarter from {:?}!", province);
            25
        }
        Loonie => 100,
        Toonie => 200,
    }
}

Here Coin:: is missing from two arms. By the way, a neat trick is to use Coin::* just before the match.

One other useful pattern to mention would be A | B => {..} when you need to handle several cases in the same way. I learned about this after being frustrated with identical lines and thinking "there must be a better way".

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.