Learning basic Rust
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:
- we use use
std::io;
as a dependency to accept the userâs input - there is a
fn() main {}
that encapuslates the code - there are
;
to end some lines (not all?) - lines are intended (reminds me of
GDScript
) afterio::stdin()
let mut guess = String::new();
creates a new mutable variable of type String.- So seems it means variables are constants per default.
let apples = 5; // immutable
let mut bananas = 5; // mutable
- comments are made of
//
which is good - variable can be initiated with a type and
::new
such as inString::new();
.read_line(&mut guess)
the&mut guess
seems to indicate a reference, immutable per default but then mutable here.expect
seems to act an as error handling a bit as a return. Program would throw a warning at compilation without it. 11 In theprintln
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.
- We now implemented the generation of the random number using
rand::thread_rng().gen_range(min..=max);
- Weâve done a loop only using
loop{}
- We accept the input as a string
- We then convert this string into a number
- We handlle non number input thanks to
match
keyword - We then compared the input with the
secret_number
It usescmp
andOrdering
- 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 instructions that perform some action and do not return a value.
Expressions evaluate to a resultant value.
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
- Feels good to have rules
- Types are important
- Think as var as immutable
cargo check, cargo run
- Return functions without , are interesting
- Shadoweing (?) vars instead of declaring new ones
- Converting types seems tricky ?
- Nice compiler helper with additional infos
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)
}