💾 Archived View for mediocregopher.com › posts › learning-rust-notes.gmi captured on 2024-08-25 at 00:01:35. Gemini links have been rewritten to link to archived content
View Raw
More Information
⬅️ Previous capture (2024-08-18)
-=-=-=-=-=-=-
Back to All Posts
How to Learn Rust
As a Go Developer
(The premise of this post is heavily "inspired" by Ian Henry's How to Learn Nix post series, which I found incredibly fun to read as an intermediate Nix developer.)
How to Learn Nix
I have been a Go developer since even before Go 1.0 was released. I was immediately taken by the simple but useful type system, strong standard library, concurrency primitives, and minimal feature-set of the language. It reminded me greatly of Erlang in those respects, with the added bonus of compiling to a binary on virtually any platform.
Learning Rust has been a perpetual item on my todo list. I've actually tried to do so a couple of times. The first time I tried was so long ago, and Rust was so new, that the hello world example didn't compile correctly due to nightly changes (or something like that). The second time was somewhere around 2015. Things had stabilized, but I still got the feeling that the language wasn't in a finished state, and for such a big language I felt like it would be better to wait some more before investing into it.
Since then there has been somewhat of a friendly rivalry between Go and Rust developers. Or maybe between just me and Rust developers. I have strong opinions about programming languages, and I'm not afraid to share them. In any case, Rust is a point of contention for me.
But now it's almost 2023, and I'm really out of excuses. Over Christmas break, I am learning Rust.
My first step in this journey is to read through The Rust Programming Language book. Or guide. Whatever it is. And I'm going to document my thoughts here as I do so.
The Rust Programming Language
My purpose here is *not* to just dunk on Rust. I *will* be pointing out things I dislike as I come across them, but many of these will also apply to Go and other languages which I use. The things which do only apply to Rust should be considered as me ~~whining~~ venting.
My purpose here is also *not* to criticize the authors of this book. I *will* be noting things which I find confusing, which may be helpful for someone who has forgotten what it's like to not know Rust.
My purpose here is, mostly, to have some fun, and keep myself engaged while I read through this thing. I will likely never look at these notes again, but I usually don't ever look at my notes again. The act of writing them is the most helpful part.
So, without further ado...
Ch 1. Getting Started
Hello world and baby's first cargo project.
- Forcing all packages to come from crates.io introduces a massive single-point-of-failure. I hope there's a way to configure alternate repos.
- It bugs me that everyone has just assumed that semantic versioning is the end-all-be-all of versioning schemes. Nothing lasts forever, folks!
Ch 2. Programming a Guessing Game
- Why does the `use rand::Rng;` at the top do anything? Something about "traits", but there's no reference back to "Rng" in any of the following code, it's just importing some hidden set of traits into scope. So one has to know both what traits `rand::Rng` deals with, and which of those the other bits of this code may or may not deal with, in order to understand what's happening?
- The way that `str.parse()` works is wild. Also I thought we were working with `String`, but now the `str` docs are being linked to for `parse()`? Not sure what's going on there.
- The errors that `expect` produces are not friendly at all, are we supposed to actually use this? It seems more akin to a Go `panic` than actual error handling.
Ch 3. Common Programming Concepts
- Don't know how I feel about shadowing, it feels a lot less constrained than I'd like. Being able to shadow to a different type feels like it's just gonna let devs be lazy with their naming.
- I appreciate that the default integer size is *not* the architecture-based one. That ambiguity in other languages has always bothered me.
- `As in most other programming languages, a Boolean type in Rust has two possible values: true and false.` LMAO, if it has more it's not a boolean!
- Characters being variable-sized unicode code points is a bold move, we'll see how it plays out.
- NGL I'm excited to work with tuples again, they might be one of my most-missed features in Go.
- `The tuple without any values has a special name, unit.` Weird... "unit" usually refers to a reference value used for measurement. An empty tuple is the explicit lack of any value, and therefore cannot be measured. Wikipedia agrees with Rust though, so wtf do I know.
- `You can also initialize an array to contain the same value for each element by specifying the initial value, followed by a semicolon, and then the length of the array in square brackets` Things I'll never ever use for 300.
- If a block ends in a line which is not terminated with a semi-colon, then that block is an expression. Could we not just say that a block is always an expression, and leave off the subtle syntax gotchas?
- The guide goes over `break` using a return value, and `break` using a label, but doesn't indicate if they can be done together. A quick test shows that they can... though probably at that point you've got some refactoring to do.
Ch 4. Understanding Ownership
Property is theft, eat the rich.
- Things are getting hairy:
let s = String::from("hello"); // s comes into scope
takes_ownership(s); // s's value moves into the function...
// ... and so is no longer valid here
// ...
fn takes_ownership(some_string: String) { // some_string comes into scope
println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing
// memory is freed.
It makes sense *why* this rule is in place, it means the compiler doesn't have to keep track of what happens to the value after it's been passed to the function, which simplifies things. It just makes me uncomfortable...
- Stack variables *can* be used after being passed into functions?? Having differing scoping rules for stack vs heap variables is really messy, I'd honestly rather if stack variables kept these same ownership-transfer rules as heap variables, even if it's technically not necessary. Or enforce some kind of naming notation between them or something.
- I appreciate that the notation for creating a reference corresponds to the type annotation for a reference type.
- `Note that a reference’s scope starts from where it is introduced and continues through the last time that reference is used.` But a normal variable's scope ends at the end of the block, not at the place it's last used? Can't we just say that everything's scope lasts until the end of its block and keep things simple?
- `Even though borrowing errors may be frustrating at times, remember that it’s the Rust compiler pointing out a potential bug early...` "If you're upset just remember that you only have yourself to blame" lol.
- I really wish the book would explain what `str` is. It's not a core data type (I guess?), and it seems to be tied into the `String` type in some way, but it's not clear. `The type that signifies “string slice” is written as &str` is the most I've gotten, but that doesn't really clarify anything because then what is just `str`??
- Something I'm gathering is that Rust isn't really "immutable by default", it instead forces *me* to *write* immutable-style code by default. The difference being that it doesn't really provide immutable data structures, it just slaps your hand if you use something in a mutable way.
- "Implicit Deref Coercions" sounds like something I'm really gonna hate, but luckily it's 11 chapters away so I'll probably never get to it. It seems that's when I would find out wtf `str` is all about, though.
Ch.5 Structs
Phew, back to easy street.
- The notation to define a struct type and to initialize said struct are essentially the same. I like this.
- `Note that the entire instance must be mutable; Rust doesn’t allow us to mark only certain fields as mutable.` Oh thank god.
- Not being able to only instantiate a subset of a struct's fields is an interesting choice. I guess because it would force there to be a null value of some sort.
- Big fan of `dbg!`.
Nothing super surprising about this chapter. I still find it weird that the scope of a variable changes depending on its type, but that's not directly to do with structs.
Ch 6. Enums
- `Programming language design is often thought of in terms of which features you include, but the features you exclude are important too.` Oh that's a good one, gonna tuck that away for later.
- (Tangent, but the book brought it up) The idea of `null` being the worst mistake in programming is basically a first year CS student meme. The reality is that null pointer exceptions are in the same class as divide by zero errors: improper handling of the zero value of a type. The real "worst mistake" is languages which for some reason allow dereferencing a pointer without any check (either at compile or runtime) for if that pointer is `null`.
Nothing really crazy in this chapter either. `if let` seems convenient.
Ch 7. Modules and Crates
- Rust has chosen the opposite meaning of "package" and "module" as Go did. This will take some getting used to.
- The difference between a crate and a package is not very clear. I expect it's something I'll figure out if I run across a package with multiple crates.
- I dunno if I've been spoiled by Go, but this module system makes no damn sense.
- Ok, I guess the `mod` directive is a kind of special inline... maybe?
- `Items in a parent module can’t use the private items inside child modules, but items in child modules can use the items in their ancestor modules.` I like dis.
I'm trying to figure out what the benefit is for having these `mod` directives, when they have to map 1:1 to a filesystem anyway. Would it be at all different to just say that all module paths starting with `crate`/`self`/`super` correspond to the filesystem tree, and provide a way for modules to declare themselves public or not?
I guess that's inconsistent with how other things are defined, if you consider that the contents of the file comprise an anonymous module, then its name and public-ness need to be defined external to it. But since the name of the file corresponds to the name of the module anyway, all that's missing is a naming convention for public-ness (files starting with `_` are private, or something like that) and you could ditch `mod` and stay within the existing pattern.
Ch 8. Common Collections
- More details about `&str`?? Nope, not really. I'm gathering that `&str` is just a pointer of sorts to any immutable string on stack or heap, and `String` is a potentially mutable string on heap.
- If the world ever switches to something that's not UTF-8 (nothing lasts forever!) Rust is gonna have a hell of a time dealing with that.
- It's weird that we're not allowed to index into a string because that might not result in a proper character, but we can range over a string and potentially panic for the same reason.
Ch 9. Error Handling
I suppose `if err != nil` isn't going to make an appearance in this section.
- Rust is able to panic if you hit an index-out-of-bounds case, which means there must be some kind of runtime check happening on every index operation. Why could the index operator not return an `Option` instead? Seems better to force the programmer to handle the case themselves rather than have these potential panics everywhere.
- I like that `main` can return a `Result`, that might prove convenient (and sensible).
- Ok so `?` is nice and all, but seems to preclude the ability to annotate errors in any way. I suppose that `match` plus a custom error type must still be used to do that. Maybe I just need to de-program my brain from the Go way, but having runtime values annotated onto errors is really nice.
- `expect` makes sense as the equivalent of doing `panic("impossible to get here")` in Go. Weird name for it though.
Ch 10. Generic Types, Traits, and Lifetimes
- Some more details on traits being brought into scope in order for their related methods to be used. It's still not clear to me why this has to be the case, if we're not required to indicate the trait involved at the point where the method is called. What if two traits have the same method name?
- `we can implement a trait on a type only if at least one of the trait or the type is local to our crate... but we can’t implement external traits on external types` I like this compromise.
- I'm curious to learn in what circumstances functions can't just assume that the lifetime of their output reference is the shortest lifetime of their input references. Similar question for structs (and I assume tuples).
- In general, I hope I don't have to actually use lifetimes much... they seem to be quite cumbersome.
Ch 11. Tests
Nothing really to say here, looks usable.
Ch 12. Example Command-line Program
My enthusiasm is waning, skipped.
Ch 13. Functional Language Features
Really burying the lead here!
- Not really related to this section specifically, but: I dunno if I don't know how to write good examples for explanations, or these are really bad. Like... this is describing some imaginary mailing list for a t-shirt company and they're doing a promotion or something, and there's users who have favorite colors? Just show me how a closure works, I'm not here for the plot!
- I guess it's not possible for an `Iterator` to terminate with an error, since it only can use an `Option`. I wonder how that effects error handling for streams of data being processed directly from a wire or file. Some searching indicates that `std::result::fold` might be the best answer.
The iterator issue will be interesting when I come to use it, though I'm used to a language which doesn't have an iterator type at all! So I suspect I'll make do.
Ch 14. More about Cargo and Crates.io
Skipped.
Ch 15. Smart Pointers
Implicit deref coersion, your time is now!
- I'm only now learning that deref-ing doesn't take ownership of the referenced value. It must copy it then, I assume? And if it has other references internally... those had better be immutable I suppose. Or I might be completely misunderstanding all of this.
- This analogy of people watching a TV to explain reference counting is pretty great.
- I don't see why a trait should bother specifying whether the receiver (`self`) is a reference and/or mutable, if that can just be undone by a `RefCell` internally anyway. Maybe I'm biased towards Go, but a trait/interface shouldn't care about internals of the things which implements it, and if you want to argue that it *should* then it seems weird to allow a backdoor like `RefCell` to exist.
Fin
There's still a couple chapters to go, but at this point I feel like I've got a good enough handle on the language to start actually using it, and I'm excited to do so. Things like concurrency and advanced pattern matching can wait.
Overall, I would describe my emotions as a mix of excitement and intimidation. I'm excited to expand my horizons a bit, and get into the weeds of a complex type system, but there's also just a lot of new things to know. New idioms, new libraries, new gotchas. And it's just a big language to start with.
Project number one: rewrite the ginger interpreter!
-----
Published 2022-12-23