For me, the biggest things to take away from these chapters were:
- Enums and pattern matching
- Borrow checker concerns emerging from these new data structures
- Derivable traits
Enums and pattern matching for the win
- That the pattern matching facility is powerful, and that enums can have associated data, structured independently for each variant ... really provides a (relatively straight forward and versatile "happy path" in rust IMO.
- I tried to hack together a file differ in rust a couple of months ago (see my post on my solution here, mostly in the comments) and found myself just leaning into enums + pattern matching and rather enjoying the process.
- So much so that major (and celebrated?) features of the language such as
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"
- Ownership and borrowing concerns are still alive here in their application to
structs
andenums
and what best practices arise out of it all.
Match statements
- In
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!) ... - so if you want a variable to live beyond the match statement, match on a reference.
EG:
let opt: Option<String> = Some(String::from("Hello world"));
match &opt {
Some(s) => println!("Some: {}", s),
None => println!("None!")
};
println!("{:?}", opt);
- There's a slightly tricky thing that happens implicitly here:
- Though the
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. - Seems tricky, but also ergonomically sensible.
- Though the
Borrowing self
in methods
Probably the trickiest and most relevant part of the two chapters
- the
self
in methods, like any other variable, can be one of three types in terms of ownership:- Owned by the method, like a plain variable
- A reference (
&self
) - A mutable reference (
&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.- EG, a method that takes
&mut self
can't be used on a variable that isn't initially mutable. - EG, a method that takes ownership of
self
effectively kills the root object, making it unusable after the method is called!!
- EG, a method that takes
-
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
- We haven't gotten to traits yet, but they come up here.
- Turns out rust has kinda has a system of inheritance for
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)- Four traits that implement methods for comparison and equality operators
Hash
for hashing an objectDefault
for defining default values
-
Of 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.