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”
-
Finish up to Chapter 2: “Programming a Guessing Game”
-
The Book: https://rust-book.cs.brown.edu/title-page.html (the special Brown University version with quizzes etc)
The Twitch Stream
- @sorrybookbroke@sh.itjust.works ran a twitch stream on these chapters which is now on YouTube: https://www.youtube.com/watch?v=ou2c5J6FmsM
- You might prefer watching and listening to that rather than reading the book.
- Be sure to catch future streams (will/should be weekly: https://www.twitch.tv/deerfromsmoke)
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”
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.
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 secondprintln!
uncommented and see the output, which is half error half linting.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 notString
, and therefore only “borrow” the variable, and then pass in&v
and notv
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')
toabc
and see if we get different printouts. As the compiler would tell us, we would need to make the argumentv
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 inmain
. And adding an additional mutation ofv
inmain
afterabc
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
It’s covered in detail in chapter 4.
Are you sure? I noticed the rust book said this:
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!).
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.
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
pass by ref / have the scope borrow a reference to the variable, or
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 insideun_authed_user
being freed too early (and thus not being available toauthed_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.
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.
Correct! Values that can be copied are copied, and those that aren’t are moved.
All primitive data types are copyable (or "are
Copy
", which will make more sense once we get to Traits). I think arrays of primitives are as well? Most everything else isn’t by default.One of my favorite parts of Rust is that to make something copyable, you “just” implement the
Copy
trait for that thing.