DEV Community

Cover image for 30 Day of Rust - Day 13

Posted on

30 Day of Rust - Day 13

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 the derive 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 and Sync are related traits. Send describes a value of a type whose ownership can be passed between threads. Most primitives are Send and the only notable type that is non-Send is the reference counter type Rc. Sync describes a value of a type that can be referenced across multiple threads. References to values that are Send yield a value which is Sync. Manually implementing these traits results in unsafe 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 and Polars?

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 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;
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."),
Enter fullscreen mode Exit fullscreen mode

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,
Enter fullscreen mode Exit fullscreen mode

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)