Hi All! Welcome to the Reading Club for Rust’s “The Book” (“The Rust Programming Language”). This is week 1 (the beginning!!).

Have a shot at going through “the reading” and post any thoughts, confusions or insights here

“The Reading”

The Twitch Stream

What’s Next Week?

  • Chapters 3 and 4
  • Start thinking about challenges or puzzles to try as we go in order to get some applied practice!
    • EG, Advent of Code
    • Maybe some basic/toy web apps such as a “todo”
  • maegul (he/they)@lemmy.mlOPM
    link
    fedilink
    English
    arrow-up
    5
    ·
    edit-2
    10 months ago

    Even though The Book is a bit verbose in these first few sections and really only touches on the basics of the language, I enjoyed going through it! Once you’ve gone to the end of just chapter 2, you’ve touched on project management with cargo, compiling with rustc or cargo, match statements, and some of the other syntactical details of the language. It’s definitely enough to get you started if you’re new to the language!


    For me, my outstanding questions, which come from the final exercise that builds a “guessing game” (code extracted below for easy reference):

    • Why are macros covered so much later in the book (ch 19)? … not for mere mortals?
    • I don’t think I really know at all what an object like Ordering actually is and what mechanically happened when it was used in the match statement.
    • Why did I import/use rand::Rng but then write rand.thread_rng().gen_range(). That I’m importing something not explicitly used in the code (Rng is never used) feels off to me. I can only guess that this was a shortcut to get to use a higher level interface. But what is Rng?
    • This will probably come up when we cover “Ownership” … but it strikes me now that we were passing variables by reference (eg &secret_number and &secret_number. Given rust’s concern with ownership and the “borrow checker” as a means of preventing memory safety issues … why isn’t it the default behaviour that a variable is passed by reference? Why do we have to explicitly pass the reference ourselves (with the ampersand syntax &secret_number)?
    use std::io;
    use std::cmp::Ordering;
    use rand::Rng;
    
    fn main() {
    
        println!("Guess the number!");
    
        let secret_number = rand::thread_rng().gen_range(1..=100);
        // println!("Secret number is: {secret_number}");
    
        loop {
    
            println!("Please input your guess.");
    
            let mut guess = String::new();
    
            io::stdin()
                .read_line(&mut guess)
                .expect("Failed to read line");
    
            // let guess: u32 = guess.trim().parse().expect("Please type a number!");
            let guess: u32 = match guess.trim().parse() {
                Ok(num) => num,
                Err(_) => continue,
            };
    
            println!("you guessed: {guess}");
    
            match guess.cmp(&secret_number) {
                Ordering::Less => println!("Too small!"),
                Ordering::Greater => println!("Too big!"),
                Ordering::Equal => {
                    println!("You win");
                    break;
                }
            }
        }
    }
    
    
    • freamon
      link
      fedilink
      English
      arrow-up
      5
      ·
      10 months ago

      This is just my attempt at answering (I’m learning too):

      Macros are easy to use, allowing beginners to write ‘hello world’ for example, but hide a bunch of complicated stuff we’re unlikely to understand until later. (I vaguely remember from Java that just writing something to the console was a whole bunch statements strung together.)

      I found it useful to document what each line was doing in the example, to get my head around the terminology.
      std is a crate, io is a module in the std crate, and that module provides methods like stdin()
      std is a crate, cmp is a module in the std crate, and that module provides enums like Ordering
      rand is a crate, Rng is a trait from the rand crate, and that trait provides methods like thread_rng()

      Ordering is a enum - a type with a list of variants with developer-friendly names. In the same way that a ‘DaysOfTheWeek’ enum would have ‘Monday’, ‘Tuesday’ …, etc, Ordering has Less, Greater, or Equal. match statements work with enums, in a ‘for this, do that’ kind of way.

      Rng is a trait, which feature a lot in Rust, but is something I’ve had difficulty getting my head around. Where I’m at so far is they mostly provide convenience. The rand create provides a number of structures in its rngs module - ThreadRng, OsRng, SmallRng, and StdRng, depending on what you want to use to generate randomness. Instead of saying ThreadRng provides the gen_range() method, and OsRng provides the gen_range() method, etc, Rust just says they implement the Rng trait. Since Rng provides the gen_range() method, it means that everything that implements it will provide it.

      thread_rng() returns a ThreadRng structure, which provides gen_range() via the Rng trait, but we need to bring that trait into scope with the use keyword in order to be able to call it.

      For the default behaviour of passing owned vs. borrowed variables, I guess it’s useful to explicitly state “I’m giving this variable to another function, because I don’t intend to use it anymore”, so that if you inadvertently do, the compiler can catch it and error.

      • maegul (he/they)@lemmy.mlOPM
        link
        fedilink
        English
        arrow-up
        2
        ·
        edit-2
        10 months ago

        Thanks!!

        Rng is a trait, which feature a lot in Rust, but is something I’ve had difficulty getting my head around. Where I’m at so far is they mostly provide convenience. The rand create provides a number of structures in its rngs module - ThreadRng, OsRng, SmallRng, and StdRng, depending on what you want to use to generate randomness. Instead of saying ThreadRng provides the gen_range() method, and OsRng provides the gen_range() method, etc, Rust just says they implement the Rng trait.

        This makes a lot of sense actually. Thanks! It does lead to the awkward situation where you’d have to know that Rng is the underlying trait of the rest of the module and so import it. But like I said, I’m guessing that’s because this example is aiming only for easy high-level usage of rand. I’d guess there’s a lower level way of using the rand crate that would involve more direct imports, perhaps use rand::ThreadRng?


        For the default behaviour of passing owned vs. borrowed variables, I guess it’s useful to explicitly state “I’m giving this variable to another function, because I don’t intend to use it anymore”, so that if you inadvertently do, the compiler can catch it and error.

        Makes sense.

        Sooo … is passing by value a thing in rust? Or does just about every method take only reference types as arguments?

        • freamon
          link
          fedilink
          English
          arrow-up
          4
          ·
          10 months ago

          Sooo … is passing by value a thing in rust? Or does just about every method take only reference types as arguments?

          I think this is an occasion where a vague familiarity with other languages ended up confusing me with Rust. The ‘&’ sign doesn’t mean ‘pass by reference’ in the same way as it does in C. Anything with a size that’s fixed at compile time is typically passed by value, whereas variables who’s size might change are passed by reference. The ‘&’ in Rust isn’t about that. For variables that are passed by reference, the ‘&’ is about whether the ownership of that memory address is transferred or not.

          To illustrate:

          fn abc(v: String) {
              println!("v is {}", v);
          }
          
          fn main() {
              let mut v=String::from("ab");
              v.push('c');
              abc(v);
          
              // println!("v is {}", v);
          }
          

          works fine as it is, but will error if you uncomment the second println! The ‘v’ variable was passed by reference, but it’s ownership was transferred, so it can’t be referred to again.

          • morrowind@lemmy.ml
            link
            fedilink
            English
            arrow-up
            2
            ·
            10 months ago

            The ‘&’ sign doesn’t mean ‘pass by reference’ in the same way as it does in C. Anything with a size that’s fixed at compile time is typically passed by value, whereas variables who’s size might change are passed by reference.

            Are you sure? I noticed the rust book said this:

            A reference is like a pointer in that it’s an address we can follow to access the data stored at that address; that data is owned by some other variable. Unlike a pointer, a reference is guaranteed to point to a valid value of a particular type for the life of that reference.

            • freamon
              link
              fedilink
              English
              arrow-up
              2
              ·
              10 months ago

              No, I’m not sure, tbh. It’s a concept I’m struggling with, and I reliant on others to correct/question me. I was trying to answer the question of whether things get passed by value or not, and I wanted to say yeah, loads of things do (anything who’s size is known at compile-time) and to caution against thinking too much in C terms.

              Here’s where my thinking is now: a variable without a & in front is passed by value. A primitive type (i.e. something who’s size is known) will be copied. So if a=4 and you pass it to a function, you can still refer to a later. A variable-length type (e.g. a String) can’t be copied, so it is moved, and referring to it later will be an error.

              A variable with a & in front is indeed a reference. It’s a memory address, so it’s of fixed size. For either a primitive or a variable-length type, the address can be copied when passed to a function, so it can be referred to again later without issue.

              This feels more correct to me, so hopefully it is. If not, I’m sure someone will have a better answer soon (this community is growing well!).

              • maegul (he/they)@lemmy.mlOPM
                link
                fedilink
                English
                arrow-up
                2
                ·
                10 months ago

                On the whole primitive types and references thing, I find it helps me to remember that a reference/pointer (subtle but importance difference between rust and C where rust has more guarantees around a pointer to make it a "reference) is also basically just a number like a “primitive” i32 etc. And references/pointers obviously (?) have to get passed by value or copied (in order to “survive” their original stack frame right?), so passing any primitive by value or copying it really isn’t different from passing it by reference, apart from when you’re running a borrow checker for managing memory of course.

              • Jayjader@jlai.luM
                link
                fedilink
                English
                arrow-up
                1
                ·
                9 months ago

                This will make more sense once we (this community) get to the 4th week/session and properly get into ownership, but lemme try to explain anyways:

                A variable without a & in front is moved into the [function’s] scope.

                Upon exiting a scope, Rust automatically drops/de-allocates any variables that were owned by/moved into said scope.

                This is why you need to either

                1. pass by ref / have the scope borrow a reference to the variable, or

                2. have your function return the variable/object/memory handle that was moved into it

                when you want a variable/some data to “out-live” being passed as argument to a function call.

                Most often you will use 1), but there are some cases where it can be much nicer to move things “into” a function call and store what you “get back out” (i.e. the return value). Using a “[Type-]State” pattern/approach is a good example of such a case (here’s a better explanation than I can give in a lemmy comment).

                Example:

                struct Unauthenticated;
                struct Authenticated { name: String };
                
                impl Unauthenticated {
                    fn login(self, username: String) -> Authenticated {
                        Authenticated { name: username }
                    }
                }
                
                pub fn main() {
                    let un_authed_user = Unauthenticated::new();
                    let authed_user = un_authed_user.login("Alice"); // `un_authed_user` has effectively been moved into `authed_user`
                }
                

                Here, we as programmers don’t need to worry about making sure un_authed_user gets cleaned up before the program exits, and we don’t need to worry about data that could have been stored inside un_authed_user being freed too early (and thus not being available to authed_user).

                Admittedly, this is a contrived example that doesn’t really need to worry about ownership, it’s just the bare minimum to illustrate the idea of moving data into and then back out of a function scope. I don’t know of a small enough “real-world” example to give here instead.

                • freamon
                  link
                  fedilink
                  English
                  arrow-up
                  2
                  ·
                  9 months ago

                  Thank you.

                  I think there’s more to it though, in that simple values aren’t moved, they’re always copied (with any & in front indicating whether it’s the value to copy or the address)

                  To illustrate:

                  fn how_many(a: u32, fruit: String) {
                      println!("there are {} {}", a, fruit);
                  }
                  
                  
                  fn main() {
                      let a=4;
                      let fruit = String::from("Apples");
                      how_many(a, fruit);
                  
                      println!("the amount was {}", a);         // this works
                      println!("the fruit was {}", fruit);      // this fails
                  }
                  
                  

                  The ‘a’ was copied, and the ‘fruit’ was moved.

          • maegul (he/they)@lemmy.mlOPM
            link
            fedilink
            English
            arrow-up
            2
            ·
            edit-2
            10 months ago

            Thanks!

            Seems I gotta dig into the borrow checker before thinking too much about this!

            Otherwise, compiling your code snippet is a nice illustration of how helpful the compiler tries to be … lots of tips in the output there! To anyone else, just try running rustc on this, with the second println! uncommented and see the output, which is half error half linting.


            For variables that are passed by reference, the ‘&’ is about whether the ownership of that memory address is transferred or not.

            Yea. So if the second println! were uncommented, how could we compile this? From what you’ve said, I’d guess that & means “borrow” (ie, not “move” ownership).

            So if we alter abc to take a &String type and not String, and therefore only “borrow” the variable, and then pass in &v and not v to pass in a “borrowed” variable, it should compile.

            fn abc(v: &String) {
                println!("v is {}", v);
            }
            
            fn main() {
                let mut v=String::from("ab");
                v.push('c');
                abc(&v);
            
                println!("v is {}", v);
            }
            

            It seems to!

            Of course, as the compiler suggests, we could instead just pass in v.clone() which presumably creates a new variable and effectively “passes by value”.


            Digging in a bit more, what happens if abc (tries to) mutate the variable?

            We can add v.push('X') to abc and see if we get different printouts. As the compiler would tell us, we would need to make the argument v mutable for this to work.

            fn abc(mut v: String) {
            
                v.push('X');
                println!("v is {}", v);
            }
            
            fn main() {
                let mut v=String::from("ab");
                v.push('c');
                abc(v.clone());
            
                println!("v is {}", v);
            }
            
            // OUTPUT:
            // v is abcX
            // v is abc
            

            I’m not clear on why I don’t have to declare that the v.clone() is mutable in anyway though.

            What about trying the same with a "borrowed’ variable?

            Well we need mutable borrowed variables, so necessary adjustments to the types of abc and its call in main. And adding an additional mutation of v in main after abc is called, and we get two different println outputs, with each mutation applying to the same variable.

            fn abc(v: &mut String) {
            
                v.push('X');
                println!("v is {}", v);
            }
            
            fn main() {
                let mut v=String::from("ab");
                v.push('c');
            
                abc(&mut v);
            
                v.push('Y');
            
                println!("v is {}", v);
            }
            
            // OUTPUT
            // v is abcX
            // v is abcXY
            
            • ericjmorey@programming.dev
              link
              fedilink
              English
              arrow-up
              2
              ·
              10 months ago

              Seems I gotta dig into the borrow checker before thinking too much about this!

              It’s covered in detail in chapter 4.

        • freamon
          link
          fedilink
          English
          arrow-up
          3
          ·
          edit-2
          10 months ago

          I wondered the same, and tried use rand::rngs::ThreadRng but the compiler wouldn’t accept it, and suggest I use the trait instead. So it looks like the compiler can be helpful in identifying these things (and maybe traits are the first thing that Rust developers look for when reading the docs for a crate).

          (wrongness edited out and hopefully corrected in new comment)

          • maegul (he/they)@lemmy.mlOPM
            link
            fedilink
            English
            arrow-up
            3
            ·
            10 months ago

            and maybe traits are the first thing that Rust developers look for when reading the docs for a crate

            Yep. Makes a lot of sense. Probably gotta start thinking in terms of traits at some point. I haven’t spun up any LSP yet but hopefully that can help surface these sorts of things.

            Still, at the moment, it does seem like a wrinkle in the usability of the language that you import something which implicitly underlies what you actually want to use.

            Also, Thanks!

    • nmtake@lemm.ee
      link
      fedilink
      English
      arrow-up
      2
      ·
      10 months ago

      The Enum Ordering provides compile-time safety. For example, if cmp() takes a string or int, the compiler can’t catch invalid inputs (“less”, “equal”, -123, …) at compile time and crash at runtime.

      • maegul (he/they)@lemmy.mlOPM
        link
        fedilink
        English
        arrow-up
        2
        ·
        10 months ago

        Hmm, not sure I’m entirely with you.

        If the argument to cmp is of an incorrect or incompatible type (where AFAIU the parent object and argument have to be the same type, eg u32), that alone will be surfaced at compile time no?

        If so, then Ordering is actually relatively trivial. It’s an enum, with variants for each possible outcome of a comparison on orderable variables (eg numbers).

        And the output of cmp is an Ordering type, which is nice for match statements, as, AFAIU, it forces us to address all possible scenarios (each being a variant of the Ordering enum).

        But the compile time safety will come from basic type checking on the argument to cmp.

        Am I off base here?

        • nmtake@lemm.ee
          link
          fedilink
          English
          arrow-up
          2
          ·
          10 months ago

          Oh I was completely wrong. cmp() takes a number (not Ordering) and returns Ordering. Sorry for bothering you.

          • maegul (he/they)@lemmy.mlOPM
            link
            fedilink
            English
            arrow-up
            2
            ·
            10 months ago

            Pretty sure that’s what this is all about! A safe space to work through the ideas and details without worrying about being wrong. I wouldn’t have understood this better if you didn’t “bother” me and now we (and anyone else reading this presumably) are both better off!