My Rust Journey - 20 Jan 2024

This is the second post about my journey learning the Rust programming language using the Rust Book. Previous posts include:

I am documenting this as I think it is a useful thing to do for people interested in learning Rust from my non-developer perspective.

At this stage, you already installed Rust on your machine and you are ready to write and run your first Rust programs.

I am using VS code with the rust-analyzer extension. I am working on an M1-mac.

The following tutorial will cover Chapter 3 of the Rust Book. It is meant to be a short summary and to be used with the book as a complementary source of information.

Mutability and Shadowing

By default, variables in Rust are immutable. This means that you cannot specify a variable and later on change it without either:

  • Shadowing or
  • Making the variable mutable

This allows you to write safe code with easy concurrency.

Let’s create a new project called chapter-3

cargo new chapter-3
cd chapter-3

Navigate to the main.rs file and add the following code:

fn main() {
   let mut x = 5; // making x mutable
   println!("The value of x is: {x}");
   x = 6;
   println!("The value of x is: {x}");
}

Note how the Rust analyzer extension will fill in (or infer) data types automatically. For example, the type for variable x is i32 (a 32 bit integer). The syntax to specify type is x: i32 = 5 or x = 5_i32.

We specified that x is mutable. If we do not do that, the program will not compile. The other option would be to shadow x by declaring the x variable a second time but with a different value:

fn main() {
    let x = 5; // declaring x the first time
    println!("The value of x is: {x}");
    let x = 6; // declaring x the second time (shadowning)
    println!("The value of x is: {x}");
}

Note how we did not make the variable mutable, and we used let to declare x a second time. This is called shadowing.

With shadowing, because we essentially create a new variable, we can change the type of the variable and reuse the same name. This is not the case with mutable variables, as we will need to keep the same type.

// a mutable variable must always have the same type declared initially
let mut x = 5; // integer type
x = “ “ // this will not compile, the compiler expects an integer and not a &str

// an immutable variable that is shadowed may not keep the same type
let x = 5; // integer type
let x = “ “ // &str type, this will compile

Note how the // syntax is used for comments in Rust. The text after the // will not be considered as code by the compiler. Also, note how the Rust analyzer will fill the type &str (string type) when we declare x the second time.

Constants

Constants are always immutable by default, and they cannot mutate. You can specify a constant as follows (within the main function):

const TWO_HOURS_IN_SECONDS: u32 = 60 * 60 * 2; // specifying a constant

println!("There are {:?} seconds in two hours.", TWO_HOURS_IN_SECONDS);

Note how constants’ types must be explicitly specified by the user; as such, the rust analyzer extension will not pick up on them. You will thus need to manually add the type u32.

Data Types

Rust is a statically typed language, meaning that the compiler must know the types of all variables at compile time. So far, we have noticed how the Rust analyzer extension is automatically inferring the types of all variables for us.

Scalar Type

A single value.

Integer

The integer type can be signed or unsigned. The i32 type means that the integer is signed (+/-) and that the integer takes 32 bits of space. The u32 means the integer is unsigned and thus will always be positive.

Integer literals

You can specify integers also as literals as follows:

let x = 1_000;
println!("The value of x is: {x}");

By default, x will have type i32. The _ can be used to improve readability for large numbers.

Integer Overflow

This happens when we, for example, have a u8 integer and we try to change its length to 256. In fact, a u8 integer can store numbers only between 0 and 255. The compiler will panic and exit with an error if we compile in debug mode (in debug mode, Rust includes checks for integer overflow). However, if we compile in release mode, Rust does not include those checks, and it does a two’s complement wrapping: number 256 will become 0, number 257 will become 1, and so on.

Floating Point

These are numbers with decimal points, they are denoted as follows:

let x = 2.1;

The variable x will have type f64 (double precision) by default.

Boolean

This is the true / false type.

let x = true;

The x variable will have the type bool.

Character

This is the most primitive alphabetic type.

let x = 'Z';

The x variable is type char. Note how char types are specified within single quotes ' ' (in contrast to string types specified with double quotes " ").

Compound Types

Group multiple values into one type.

Tuple Type

Groups different types into one compound type, and it has a fixed length (cannot grow or shrink in size).

let tup = (100, 1.1, 'Z');
let (x, y, z) = tup; // destructuring the tuple
println!("The first element in the tuple is: {:?}.", tup.0); // this will print the first tup element

The tup tuple has elements of different types: i32, f64, and char.

Array Type

Each element within the array must have the same type. An array has a fixed length (while vectors are allowed to grow or shrink) and it is stored on the stack memory (more on this later on).

let a = [1, 2, 3];
let first = a[0]; // declaring a new variable using the first element in the array

The a array has elements of the same type. You will notice how the Rust analyzer will automatically fill in [i32; 3] specifying the type of the elements in the array and the number of elements in the array separated by a semicolon.

Functions

The function below has two parameters:

  • x is a signed 32-bit integer i32
  • unit is a string &str

Since the function returns a value, we also need to specify the type of the return value with the -> notation. Note how each line except for the final one ends with semicolon, meaning they are statements. The last line does not end with a semicolon, and in Rust this is defined as expression. Statements do not return values while expressions do. Declaring a variable is a statement.

fn another_function(x: i32, unit: &str) -> i32 { // specifying the return type

    let y = x * 2; // statement doing something with x

    println!("The weight is {}{}.", y, unit); // statement printing x and y

    y // return value; this is an expression

}

The function above can be used within the main function in various ways.

another_function(5, "kg"); // we can used it on its own
let x = another_function(5, "kg"); // we can use it to declare a variable

Since the function returns a value, we can use it to declare a variable. We would not be able to do so if the function ends with a statement because we would not have anything to bind to the variable.

Control Flow

If Statements

In Rust, as in any other programming language, you can make the code execute something depending on some conditions.

The code below declares a condition as bool type. It then uses the condition to declare a new integer variable number. Note that both the if and else arm must have value types that are the same. The code then prints the number value. After that we have a conditional statement using the AND && and OR || logical operators. The first condition checks if number is divisible by 2 and 1, the second condition checks if it is divisible by 2 or 1, and the last condition is executed if the number is not divisible by 2 or 1. The code stops when it encounters a condition that is true.

let condition = true; // bool variable
let number = if condition {1} else {2}; // declare variable using if statement

println!("The value of the number is: {}", number); // print the value of number

if number % 2 == 0 && number % 1 == 0 { // && (and) condition
    println!("The number is divisible by 1 and 2.");
} else if number % 2 == 0 || number % 1 == 0 { // || (or) condition
    println!("The number is divisible by 1 or 2.");
} else { // remaining cases
    println!("The number is not divisible by 1 or 2.");
}

Loops

Loop

There are three ways of generating loops in Rust: loop, for loop, while loop.

Below there is a nested loop with annotations. The loop counts from 0 to 2. It counts up when the inner loop countdown reaches 9 starting from 10. Note the loop label counting_up for the outside loop. This can help when you have multiple loops.

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

    loop { // inner loop
        println!("remaining = {remaining}");
        if remaining == 9 {
            break; // exiting the inner loop
        }

        if count == 2 {
            break 'counting_up; // exiting the outer loop
        }

        remaining -= 1; // counting down
    }

    count += 1; // counting up

}

print!("End count = {count}");

Use loop when you need an infinite loop or when the exit condition is complex and cannot be expressed in a single expression. It is commonly used for tasks such as event loops, where the loop runs indefinitely until explicitly interrupted.

Another simpler example using loop: counting down from 3 to 1, and printing GO!!!.

let mut number = 3;

loop {

    println!("{number}");

    number -= 1;

    if number == 0 {
        break
    }

}

println!("GO!!!");

Using While

The while loops run while a condition is held true, after which they automatically break.

The while loop below counts down from 3 to 1, it breaks when number is 0 and prints GO!!!. Note how, compared with the previous example, the code is simpler.

let mut number = 3;

while number != 0 {

    println!("{number}");

    number -= 1;

}

println!("GO!!!");

Use while when the loop condition depends on a specific expression or needs to be dynamically determined during runtime.

For loops

For loops are useful when you already know the number of iterations you would like to do in your loop. Below the same example as before using a for loop. Note how we use Range syntax (n..N) provided by standard library, which generates number starting from n ending with N-1. The rev reverses the range.

Note how, compared with the previous examples, the code is simpler.

for number in (1..4).rev() {

    println!("{number}");

}

println!("GO!!!");

Use forloops when you have a known range, collection, or iterator that you want to iterate over. It's often more concise and idiomatic when working with known ranges or collections.


So far we learned the concepts of mutability and shadowing, variables and constants, scalar and compound data types, functions, control flow with conditional statements and loops. See you in the next post!

0
filippoweb3Post author

🔥 Web3 explained from the non-developer's POV. 🚀 Helping Polkadot users explore the ecosystem with confidence. I post daily on socials. Opinions are mine.

16 comments

Loading replies...

🔥 Web3 explained from the non-developer's POV. 🚀 Helping Polkadot users explore the ecosystem with confidence. I post... Show More