Learning Rust - Rust Book Chapters 1-3

Learning Rust - Rust Book Chapters 1-3

I've been writing Rust for a couple of months now but noticed I was quickly starting to run into a lack of knowledge on some foundational topics that was leading to frustrating bugs and annoying dead ends. So, instead of trying to forge ahead in the hopes that I could get by and learn as I go, I've decided to take some time to go back to basics by reading through the Rust book.

I decided to skip over most of Chapter 1. There's not too much of a point in writing a blog on how to install Rust... nothing really to gain or discuss there. That said, I do want to talk a bit about Cargo and my experience with it.

Cargo, frankly, is one of the bigger reasons I was drawn to Rust after experiencing low-level programming in C/C++. Dependency handling is an incredibly important topic in modern languages, and it's a big reason why people are drawn to the world of Node and NPM. Being able to install, update or remove a package or library in one terminal command is incredibly nice. Cargo's dependency handling alongside its ability to build and run the software itself makes it a fantastic developer experience.

Since I've already made the NPM comparison, I'll also say that the Cargo.toml developer experience is significantly easier, if maybe less powerful, than dealing with package.json. The syntax makes sense, there's far less boilerplate to memorize and it's way easier to read. So, kudos to Rust for that one as well.

On to Chapter 2 and 3!

Chapter 2 ran through some of the basics of a Rust program, including importing the std::io library for user input and a (brief) introduction to references and mutability. I wouldn't say skip chapter 2 per se, but it was kind of odd to put Chapter 2 before Chapter 3, when Chapter 3 is a good intro to the concepts that are put into practice in Chapter 2... guess that might be a stylistic difference, I dunno.

Chapter 3 introduces the core concepts that are foundational to any programming language:

  • Variables and mutability
  • Data types
  • Functions
  • Comments
  • Control flow

The bit about mutability is pretty vital to understanding Rust: variables are immutable by default and have to be declared mutable via the mut keyword to be made mutable (or changeable) further in the program. Variable mutability is right up there with semi-colons as one of the most annoying sources of easy compiler errors in my experience, but mutability is a key concept in the secure design of the Rust programming language, so it's worth learning.

This chapter introduces constants as well, which are useful in that they are entirely immutable variables that are calculated at compile time. Basically, an immutable variable can be calculated at run time as long as it isn't changed later in the program, like so:

let x = 5;
let y = x+5 // y is immutable (as is x) but it is calculated at run time

So in the above example, y is immutable and is (theoretically) calculated at runtime. You can't change it later in the program, but if x were more variable (as in affected by user input or some other state) then y would be calculated differently at runtime.

Constants are different. They're immutable (like variables are by default) and can't be made mutable, but their value is calculated at compile time, meaning they can't be variable at all... thus the name constant.

You also need to declare their data-type inline explicitly, instead of declaring it implicitly like other variables.

use std::io;

let x = 5; // x is made a u32-type variable by default, implicitly, because of the value that is assigned to it.

let y = x+5; // y is also made a u32-type variable by default, implicitly, because x is a u32 type and so is 5

const y: u32 = 17; // y is a constant, so the variable needs to be explicitly typed

let mut stringvar = String::new();
let b = std::io::readline(&mut stringvar).expect("String is invalid!"); // Assigning user input to b at runtime

const z: String = b; // This wouldn't be allowed: at compile time, the value of b is unknown, so it can't be assigned to a constant

This is a pretty important distinction between immutable variables and constants, and it definitely helped to read over the book to really understand it.

The section in Chapter 3 on functions also contained an interesting subsection on statements versus expressions. In Rust (and probably other languages I'd imagine) expressions are lines of code that return a value. They do not end in semi-colons and can be bound to variables. Statements are lines that do not return values and do end in semi-colons. What this means is you can get interesting pseudo-function like behavior like so:

fn main() {
    let x = {
        let y = 17;
        let z = 32;
        y+z
    };
    println!("The value of x is {x}");
}

In this main function, everything in the brackets is an expression. The reason it ends in a semi-colon is because it's part of the let x = ...; statement. So the code block within the curly-braces is its own expression that evaluates to y+z. It also has its own scope: y and z are not accessible outside of the ending curly brace. So you essentially have a way of establishing anonymous functions in this way and assigning the returned value to a variable. Notice that the type is also implicitly determined as well.

Finally, control flow. Rust has all of the normal control flow loops: for, while, forEach, etc. It also has one that I at least haven't used, the generic loop loop. Apparently it's a way of running a loop that doesn't require you to set up start and end conditions, you just have to either deal with it being an infinite loop or create your own breaking statements. This loop loop can also return variables:

fn main() {
	let mut index = 0;
	let x = loop {
    	index+=1;
        if index > 10{
        	break index*2;
        }
    }
}

This loop would increment the index until it's greater than 10 and then return the value of index*2 and result would hold that number.

I personally like loop loops and will probably start implementing them any time I don't explicitly need the types of controls that for and while loops intend to create. One of the other cool things about loop loops is that you can name and nest them:

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {count}");
        let mut remaining = 10;

        loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {count}");
}

In the above example, there are two, nested loop loops (man that's getting tiring to type...): one generic loop on the inside of another loop named counting_up with a ` next to it. There are then two breaking statements: one generic break that just says break; and another that says break `counting up; (can't format that because it uses the ` character...).

The lattermost break breaks out of the whole external loop, while the normal break breaks out of just the innermost loop. This is because the outermost loop is named `counting_up so that you can explicitly break out of that loop if count == 2.

This is really cool and I'm assuming is something borrowed from functional programming. I can think of a lot of instances where that would have made my code more readable, both to me and to other observers.

--

That's all for now. I learned a ton from Chapters 1-3, but I know that the concepts are going to get a good bit more advanced from here on out. I hope this article has helped you grasp some of the concepts and I look forward to seeing what the rest of the Rust book has to offer!