Finally, we can make our own types (or data structures)!!
This is supplementary/separate from the Twitch Streams (see sidebar for links), intended for discussion here on lemmy.
The idea being, now that both twitch streams have read Chapters 5 and 6, we can have a discussion here and those from the twitch streams can have a retrospective or re-cap on the topic.
This will be a regular occurrence for each discrete set of topics coming out of The Book as the twitch streams cover them
With Ch 4 on the borrow checker out of the way, chapters 5 & 6 feel like the “inflection point” … the point where we’re ready to actually start programming in rust.
Custom types, data structures, objects with methods, pattern matching, and even dipping into rust’s traits system and it’s quasi answer to class inheritance.
If you’re comfortable enough with the borrow checker, you can really start to program with rust now!
I personally didn’t think this content was difficult, though it prompts some interesting points and topics (which I’ll mention in my own comment below).
- Any thoughts, difficulties or confusions?
- Any quizzes stump you?
- Any major tips or rules of thumb you’ve taken away or generally have about using
structs
andenums
?
For me, the biggest things to take away from these chapters were:
Enums and pattern matching for the win
Option
andResult
types are really just applications ofenums
(along with rust’s good type system)The example in the book of the
IP Address
enum
type is quite a nice demonstration I think:enum IpAddr { V4(u8, u8, u8, u8), V6(String), } let home = IpAddr::V4(127, 0, 0, 1); let loopback = IpAddr::V6(String::from("::1"));
We’re still learning “The Borrow Checker”
structs
andenums
and what best practices arise out of it all.Match statements
match statements
, the concerns are relatively straight forward (I think). Match arms take ownership of the variables they “use/touch” (I’m still unclear on the details there!) …EG:
let opt: Option<String> = Some(String::from("Hello world")); match &opt { Some(s) => println!("Some: {}", s), None => println!("None!") }; println!("{:?}", opt);
match
is on&opt
, thes
in the patternSome(s)
is also a reference because rust implicitly “pushes down” the reference from the outer enum to the inner field or associated data.Borrowing
self
in methodsProbably the trickiest and most relevant part of the two chapters
self
in methods, like any other variable, can be one of three types in terms of ownership:&self
)&mut self
)struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } fn set_width(&mut self, width: u32) { self.width = width; } fn max(self, other: Rectangle) -> Rectangle { Rectangle { width: self.width.max(other.width), height: self.height.max(other.height), } } }
What’s tricky about this is that a method’s signature for
self
has consequences that both reach back to the initial type of the root object (ie, is it mutable or not) and forward to what can be done with the root type afterward.&mut self
can’t be used on a variable that isn’t initially mutable.self
effectively kills the root object, making it unusable after the method is called!!I’m sure there are a bunch of patterns that emerge out of this (anyone with some wisdom here?) …
But the simple answer seems to borrow
self
, and if necessary, mutably borrow.Taking ownership of
self
is an interesting way to enforce a certain kind of usage and behaviour though.As the object dies, the natural return of an
owning method
would be a new object, probably of the same type.Which leads into a sort of functional “pipe-line” or “method chaining” style of usage, not unlike the “Faux-O” idea in Cory Bernhardt’s talk Boundaries. It’s likely not the most performant, but arguably has some desirable qualities.
Derivable Traits
structs
where atrait
can be easily implemented for astruct
“automagically”:#[derive(Debug)]
EG:
#[derive(Debug)] struct Rectangle { width: u32, height: u32, }
This particular trait,
Debug
, allows for the printing of a struct’s full makeup withprintln!
.All of the “Derivable” traits (from the std lib) are listed in Appendix C of The Book
There aren’t that many, but they’re useful:
Copy
andClone
enable a struct to be copied without having to worry about ownership (though you have to be careful about the types of the fields, as its theircopy
methods that are ultimately relied on)Hash
for hashing an objectDefault
for defining default valuesOf course, when we cover traits we’ll learn how to implement them ourselves for our custom types, but these seem to be fundamental features of the language, and easy enough to use right away.
The classical one is something that looks like the following:
struct LoggedOut; struct User {name: String}; struct Admin {name: String}; impl LoggedOut { fn login(self, name: String, password: String) -> User { User { name } } fn admin_login(self, name: String) -> Admin { Admin { name } } } impl User { fn log_out(self) -> LoggedOut { LoggedOut {} } } impl Admin { fn log_out(self) -> LoggedOut { LoggedOut {} } } fn fetch_user_preferences(user: User) { /*...*/ } fn do_admin_action(admin_account: Admin) { /* ... */ } fn main() { let mut user_session = LoggedOut {}; /* (get user input) */ match input { "login" => { user_session = user_session.login(name, password); } "admin" => { user_session = user_session.admin_login(name); } } }
This would prevent you from writing code that uses the user’s info / session “object” after they have logged out (and/or before they have logged in). On its own it’s naive and a bit encumbering - I expect an enum would make more sense but then you can’t restrict via types which user session values can and can’t be passed to specific functions. In any case, when you are building a whole system around it it can be very useful to have the compiler enforcing these sorts of things “in the background”.
This is basically what Gary Bernhardt is talking about in the talk you linked.
Yep. And then you realise that “move semantics” aren’t just a safety net that you have to “fight with” but actually a language feature against which you can develop/deploy desirable patterns.
A minor thing I noted reading your code snippets was that I immediately noticed, like at a gestalt level, the lack of ampersands (
&
) and therefore references and could immediately tell that this was a “faux-O”/pipline style system. Not too bad for a syntax often derided as messy/bad.