mar1.dev

đŸŒŒ About đŸ§± Notes

Learning basic Rust

📚 Reading time: 60 minutes
 

Some setup things

As when any story starts, the future looks bright. I started from the roots wit The Rust Book chapters 1-2-3. Installation is super simple on mac, to be honest. Just a simple one-liner:

curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

Migh also need a C compiler by adding

code-select --install

You can check if everything went smoothly by doing

rustc --version

If you have troubles with your %PATH% after running the previous command (meaning rust is not recognized), please check this stackoverflow’s link.

Ok now we can create some main.rs file and put some code int it:

fn main() { println!("Hello, world!"); }

Hello, world! Nothing very surprising for the code above, expect the ! after the function’s name. Actually, that’s because println is not a function but a

macro

I guess I learn more later about them. Big things here coming from JS/Node is the compilation step. Not that big tho, it’s literally 1 command line.

rustc main.rs

And I guess that’s the wrap on how to create and compile a first Rust program. Now come cargo! It seems it’s a bit like npm for node. Use is pretty straightforward. To create a project:

cargo new project_name

It creates a cargo.toml file, as configuration, and a /src folder where our main.rs file file is waiting for us. We also have

cargo check
cargo run
cargo build

to respectively check if the code compiles, if it runs properly and finally to compile it. Seems to be a good habit to do a lot of cargo check for validation before building.

The guessing game code

    use std::io;

    fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
    }

Few things to observe here:

  1. we use use std::io; as a dependency to accept the user’s input
  2. there is a fn() main {} that encapuslates the code
  3. there are ; to end some lines (not all?)
  4. lines are intended (reminds me of GDScript) after io::stdin()
  5. let mut guess = String::new(); creates a new mutable variable of type String.
  6. So seems it means variables are constants per default.
let apples = 5; // immutable
let mut bananas = 5; // mutable
  1. comments are made of // which is good
  2. variable can be initiated with a type and ::new such as in String::new();
  3. .read_line(&mut guess) the &mut guess seems to indicate a reference, immutable per default but then mutable here
  4. .expect seems to act an as error handling a bit as a return. Program would throw a warning at compilation without it. 11 In the println return, var is surrounded by {}
let x = 5;
let y = 10;

println!("x = {x} and y + 2 = {}", y + 2);

Would return: x = 5 and y + 2 = 12.

So we can use {} as crab’s bracket to send the value of the operation.

I’m adding rand = "0.8.5" to the Cargo.toml file to add a library crate. It’s like a dependency used in my binary crate. Here the goal is to create randomness, a bit like Math.Random() would act.

It has to be used to compile

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You entered: {guess}");

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

Here is the complete code for the “Guess The number” game.

  1. We now implemented the generation of the random number using rand::thread_rng().gen_range(min..=max);
  2. We’ve done a loop only using loop{}
  3. We accept the input as a string
  4. We then convert this string into a number
  5. We handlle non number input thanks to match keyword
  6. We then compared the input with the secret_number It uses cmp and Ordering
  7. If the guess is correct, we break the loop

This program was very interesting as it teaches us a lot about functions, variables, dependancies and building process using Rust language.

Common Programming Concepts

Variables

First principle is that variables are per default immutable in Rust. You declare a variable using let You can not alterate the value once you attribute it.

However when mut is used, variables can become mutable.

Incorrect code

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

^^^^^ cannot assign twice to immutable variable

Correct Code

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

Constants

Constants also exist and are always immutable. Logical, so you can not use mut

Also, constants have a type and be declared with const

const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

Rust’s naming convention for constants is to use all uppercase with underscores between words.

Constants are valid for the entire time a program runs, within the scope in which they were declared.

Shadowing

This concept is about re-declaring values with let.

fn main() {
    let x = 5;

    let x = x + 1;

    {
        let x = x * 2;
        println!("The value of x in the inner scope is: {x}");
    }

    println!("The value of x is: {x}");
}

Shadowing is different from marking a variable as mut because we’ll get a compile-time error if we accidentally try to reassign to this variable without using the let keyword.

By using let, we can perform a few transformations on a value but have the variable be immutable after those transformations have been completed.

We can also change the type of the var.

Data Types

Rust being a statically typed language, the compiler must know the type of the var when it compiles.

let guess: u32 = "42".parse().expect("Not a number!");

Thats what the :u32 is here for.

Scalar Types

A scalar type represents a single value. Rust has four primary scalar types: integers, floating-point numbers, Booleans, and characters.

Integers

i8,i16,i32,i64,i128

u8,u16,u32,u64,u128

Floating Point Number

f32: single digit precision

f64: double digits precision

fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

You can perform classic math operations on numbers: + / * -

Boolean

bool: true/false

fn main() {
    let t = true;

    let f: bool = false; // with explicit type annotation
}

Char

fn main() {
    let c = 'z';
    let z: char = 'â„€'; // with explicit type annotation
    let heart_eyed_cat = 'đŸ˜»';
}

Four bytes Unicode Scalar Value. Had to be declared with ” contrary to strings ""

Compound

Compound vars can have multiple types into one type. They can be turples and arrays.

Tuples

A tuple is a general way of grouping together a number of values with a variety of types into one compound type. Tuples have a fixed length: once declared, they cannot grow or shrink in size.

Declaring a tuple

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

The tuple is declared with each type of its components. Here the var is made of 3 different numbers. Or at least that was I was used to think about them.

To access a tuple element, we can destructure it or access it through its element index.

Destructurate way

fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

    println!("The value of y is: {y}");
}

Where x y z represents the appropriate elements.

Indexed way

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;
}

We can notice here that 0 is in fact the first element. Reminds of JS again.

The array

Array are different from tuples because all the elements have the same type.

They’re similar in the way the’re also fixed sized.

Declare an array

fn main() {
    let a = [1, 2, 3, 4, 5];
}

So we’re classicaly using [ ] notation for the declaration. Reminder: It was ( ) for the tuple.

If needed to specify the type of the elements (not sure yet tbh) you’ll have to do:

let a: [i32; 5] = [1, 2, 3, 4, 5];

Where i32 is the type, 5 the number of elements. Reminder: arrays are fixed sized.

Accessing an array element is pretty easy:

let a: [i32; 5] = [1, 2, 3, 4, 5];
let first = a[0]
let second = a[1]

It seems if I try to access an element which is outside of the index, the app will compile but panick and crash.

Functions

Conventions

Functions names are snake case: underscore + _ instead of space

fn another_function() { }

We can call one function into another one.

fn main() {
    println!("Hello, world!");

    another_function();
}

fn another_function() {
    println!("Another function.");
}

Arguments

Functions can have arguments/parameters inside the parenthesis.

fn main() {
    another_function(5);
}

fn another_function(x: i32) {
    println!("The value of x is: {x}");
}

Here we call another_function with 5 as a number in main().

Below, we declare another_function, taking a i32 variable type named x. Then, it’ll display its value called in main().

Arguments types in function declarations are mandatory in Rust.

If there are several arguments, there are separed by a comma (in call as declaration).

fn main() {
    print_labeled_measurement(5, 'h');
}

fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("The measurement is: {value}{unit_label}");
}

Statements and Expressions

Function bodies are made up of a series of statements optionally ending in an expression.

Because Rust is an expression-based language, this is an important distinction to understand.

Statements are block scoped.

Functions that return values

In Rust, the return value of the function is synonymous with the value of the final expression in the block of the body of a function.

fn five() -> i32 {
    5
}

fn main() {
    let x = five();

    println!("The value of x is: {x}");
}

The five function has no parameters and defines the type of the return value, but the body of the function is a lonely 5 with no semicolon because it’s an expression whose value we want to return.

If we want to use an argument and still use the final return we can do like this

fn main() {
    let x = plus_one(5);

    println!("The value of x is: {x}");
}

fn plus_one(x: i32) -> i32 {
    x + 1
}

We take a i32 as x parameter and return a i32 non named of x incremented of 1.

Important: If x + 1 had a final , the compiler would throw an error.

Interesting point here. We have to think about ending return value of every functions.

Comments

Comments in code are important. Right?

// yes
// they
// are

No really, they do.

Here it seems we use only // even for multiple lines.

Prefer writing them on a line before than at the end of the line.

Nothing complicated.

Control Flow

if condition

fn main() {
    let number = 3;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

Chaining if/else:

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

Use with boolean:

fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

    println!("The value of number is: {number}");
}

Loop

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {result}");
}

Interesting: the result is a loop that returns a value after the break.

While

Maybe the best loop to learn first?

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{number}!");

        number -= 1;
    }

    println!("LIFTOFF!!!");
}

Looping over an array

fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        println!("the value is: {}", a[index]);

        index += 1;
    }
}

The for loop, classic

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("the value is: {element}");
    }
}

Clear and efficient. Not dependant on the index so i’d say it’s way better for an array to remember. For is now the best loop.

Thoughts

Practice

Here is the final program I managed to create on my first day of Rust. Please don’t judge me. Every long journey starts with a small step. Anyways, it was a very, yet intense day. Probably studied for 8 hours.

// require crate
use std::io;

// const declaration
const GLMR_PRICE: f64 = 0.23;

// first function executed
fn main() {
    // loop is making the menu consistant
    loop {
        // Display Modes
        println!("Choose a mode:");
        println!("1. USD TO GLMR");
        println!("2. GLMR TO USD");
        println!("3. APY CALCULATOR");
        println!("4. Exit");

        // Ask the user for a mode
        let mut input = String::new();
        io::stdin().read_line(&mut input).expect("Failed to read line");

        // Parse the input as an integer and check if it matches
        match input.trim().parse::<u32>() {
            Ok(mode) => {
                match mode {
                    1 => {
                        mode_a();
                        continue;
                    }
                    2 => {
                        mode_b();
                        continue;
                    }
                    3 => {
                        mode_c();
                        continue;
                    }
                    4 => {
                        println!("Exiting...");
                        break;
                    }
                    _ => println!("Invalid mode choice!"),
                }
            }
            Err(_) => println!("Invalid input! Please enter a valid number."),
        }
    }
}

fn read_input() -> f64 {
    let mut input = String::new();
    io::stdin().read_line(&mut input).expect("Failed to read line");
    input.trim().parse().expect("Invalid input! Please enter a valid number.")
}


fn mode_a() {
    println!("How many dollars do you have?");

    let guess = read_input();
    let guess = dollars_to_glmr(guess);
    println!("You could also have {guess} GLMR");
}

fn mode_b() {
    println!("How many GLMR do you have?");

    let guess = read_input();
    let guess = glmr_to_dollar(guess);
    println!("You could also have {guess} USD");
}

fn mode_c() {
    println!("Welcome to the Weekly APY Calculator!");

    println!("Enter the initial amount:");
    let initial_amount: f64 = read_input();

    // Read the annual APY as a percentage from the user (number is needed)
    println!("Enter the annual APY:");
    let annual_apy: f64 = read_input();

    // Calculate future amount and earned amount after different time frames
    // First tuple definition in rust!
    let (future_amount_daily, earned_amount_daily) = calculate_future_and_earned_amount(initial_amount, annual_apy, 365);
    let (future_amount_weekly, earned_amount_weekly) = calculate_future_and_earned_amount(initial_amount, annual_apy, 52);
    let (future_amount_monthly, earned_amount_monthly) = calculate_future_and_earned_amount(initial_amount, annual_apy, 12);
    let (future_amount_yearly, earned_amount_yearly) = calculate_future_and_earned_amount(initial_amount, annual_apy, 1);

    // Display the results
    println!("Future Amount after one day: {:.2}", future_amount_daily);
    println!("Earned Amount after one day: {:.2}", earned_amount_daily);

    println!("Future Amount after one week: {:.2}", future_amount_weekly);
    println!("Earned Amount after one week: {:.2}", earned_amount_weekly);

    println!("Future Amount after one month: {:.2}", future_amount_monthly);
    println!("Earned Amount after one month: {:.2}", earned_amount_monthly);

    println!("Future Amount after one year: {:.2}", future_amount_yearly);
    println!("Earned Amount after one year: {:.2}", earned_amount_yearly);
}

// simple fct to return a number
fn dollars_to_glmr(x:f64) -> f64 {
    x / GLMR_PRICE
}

// reverse side, probably a way clever way of doing
fn glmr_to_dollar(x:f64) -> f64 {
    GLMR_PRICE * x
}

// Take 3 parameters, return a tuple of two f64 numbers
fn calculate_future_and_earned_amount(initial_amount: f64, annual_apy: f64, time_frames: u32) -> (f64, f64) {
    // Convert annual APY to the appropriate time frame
    let apy_adjusted = (annual_apy / 100.0) / (time_frames as f64);

    // Calculate the future amount
    let future_amount = initial_amount * (1.0 + apy_adjusted);

    // Calculate the earned amount (interest earned)
    let earned_amount = future_amount - initial_amount;
    // notice no , in the next line: it return a value
    (future_amount, earned_amount)
}