Hello world,
I'm back from my day off yesterday (recovering from our company's Spring Party). I've had a think about what I've learned so far and talked to some other Rust learners about what to do next. Over the last couple of days I've been writing about my work on a CLI tool. I feel that my learning has slowed down and that I've bitten off too much in one go. Most of the time I found myself battling with the compiler to get simple got to work, and I found understanding documentation of APIs very challenging. Additionally, the kind of debugging I was doing didn't really help me draw useful conceptual conclusions which I could mention here on my blog.
Learning needs to be enjoyable and I want my blog to be a space to help me and you, dear reader, learn stuff about Rust. With both these things in mind, I've dropped working on that project for now. Instead I've started working my way through the Rust book.
Yesterday's questions answered
- To quote from the Rust Book: The
derive
attribute generates code that will implement a trait with its own default implementation on the type you’ve annotated with thederive
syntax. This is different from class inheritence. - Returning an implementation type simply means to return a type that implements a given trait. This is a way to make functions generic and to decouple them from concrete implementations in your code.
- A single quotation
'
before a function argument indicates its lifetime to the Rust compiler. The reason why this is only sometimes required is because the Rust compiler can derive the lifetime in many cases, as it has a set of rules to for an input's and output's lifetime. You as the developer only need to step in when these rules are sufficient for the compiler. There is an entire subchapter in Rust book on this topic. -
Send
andSync
are related traits.Send
describes a value of a type whose ownership can be passed between threads. Most primitives areSend
and the only notable type that is non-Send
is the reference counter typeRc
.Sync
describes a value of a type that can be referenced across multiple threads. References to values that areSend
yield a value which isSync
. Manually implementing these traits results inunsafe
Rust.
Today's open questions
As I really just went over some basics in the Rust book today, I don't have any burning questions. Some general questions I'll look at tomorrow:
- How do you indicate to
cargo
what your project documentation is? - What are some Python projects using Rust other than
Pydantic
andPolars
?
Back to basics
Going through the introduction of the Rust book I learned a coupe of cool little features. First of all, once you've installed Rust, you can access the Rust Book at any time by running the following command:
rustup docs --book
This could be useful for dedicated students who want to access their learning materials where there is no reliable wifi (e.g. when travelling on Deutsche Bahn).
Now the Rust book is pretty useful, but it's not going to help you understand external crates out of the box. But don't fear, Rust's package manager cargo is there to help. To get all the docs of the dependencies of your project as a file displayed in your browser, run:
cargo docs --open
Cargo cares about project structure
Whenever you initiate a project using cargo new
, it's important to note that cargo
expects your code to be in a src
folder. Also cargo run
will always searching main.rs first, when the program is designed to yield an executable. Both of these facts demonstrate how Rust pushes developers to integrate best practices when structuring their code.
By default, cargo will also build a debug version of your executable. If you want an optimised release version, you can use the --release
option.
Cargo uses the Cargo.lock
file to guarantee reproducible builds. This means that developers should check the lock file into version control. Also, the Cargo.toml
syntax implicitly using the ^
symbol, meaning that cargo will look for the highest minor version update when installing the package. This behaviour is the same when running cargo update
.
Combining ordering with match
The standard library's Ordering
enum provides a very readable interface for comparing values in a match statement, consider:
use std::Ordering;
const REQUIRED_RESPONSE_NUMBER: usize = 10;
let responses = get_responses();
match responses.len().cmp(&REQUIRED_RESPONSE_NUMBER) {
Ordering::Greater => println!("Too many responses received"),
Ordering::Less => println!("Too few responses received"),
Ordering::Equal => println!("Correct number of responses received."),
};
The Ordering
enum reduces the amount of code duplication that may often be required when comparing multiple cases. Not only do we avoid having to use a variable representing the case in each arm, but we also can skip on repeating the value we want to compare against.
Matching in loops
Another convenient way to use match
is in the control flow of a loop
.
user_inputs: [&str] = get_inputs();
for user_input in user_inputs.iter() {
match is_valid_input(user_input) {
Ok(_) => break,
Err(__) => continue,
}
}
Now here we don't really do anything too realistic when we get a valid a valid input. But the negative case could occur in a number of scenarios in a program. And really the learning here is really to try and think of Rust programs in terms of cases and outcomes. Dynamically typed languages like Python don't force this upon. And simple applications may get away with not handling edge cases or errors. But Rust encourages (and to some extend forces) you to think in these terms. Of course, the compiler helps you out a bunch, but it still takes time and energy to think through many of the possible execution paths of your program every time you write code.
Top comments (0)