Table of contents
- What is Rust?
- Installation
- Text editors
- Directory structure
- Hello World
- Variables and their types
- println, print and text formatting
- Operators
- Flow Control
- Loops
- Strings
- Tuples
- Arrays
- Vectors
- Functions
- Structs
- Enums
- Comments
- Generics
- Cargo
- pub and mod
- Final Steps
What is Rust?
Rust is a multi-paradigm general purpose language which provides memory safety out of the box. It's similar to C++.
It's a statically and strongly typed language and it's a compiled language.
It's a low level language and was created to have similar performance as C and C++ with added benefits like code safety and memory safety.
Installation
Windows
Refer to Rust Official Installation Page and download 64bit or 32bit version for your system.
After downloading the installer, double click and follow the instructions on screen.
Download Microsoft C++ Build Tools Installer and install the tools.
Finally, open your command prompt and run
$ rustc
If you get an ouput from this command, congrats, rustc is installed.
Mac/Linux
Run the following command in your terminal
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Follow the instructions on screen after running the command.
Once the installation is finished, restart your terminal
Finally, run
$ rustc
If you get an ouput from this command, congrats, rustc is installed.
Alternatively, you can use your package manager to install rust.
Text editors
I personally use neovim and VSCode but you're free to use whichever text editor you want. Some notable ones are
- Notepad++ (Windows Only)
- Micro (Terminal based)
- Brackets
- Sublime Text
- Atom
- Your native notepad application
Directory structure
I have created a folder named rust
and two more folders inside that are named src
and a build
rust
├── build
└── src
Hello World
Rust files end with .rs
extension. So let's create a file named main.rs
inside src
folder
The main file should have only one main function (Just like C/C++)
Let's create our main function
// main.rs fn main() { }
fn
is the keyword to declare our functions, it is followed by our function namemain
with parentheses and finally braces which are our function's body.We'll get into functions later on, for now all you need to know is that a
main
function is required for us to compile the program; it's our entry point.Now let's print hello world
// main.rs fn main() { println!("Hello World"); }
println
is a macro hence why it's followed by an exclamation mark!
.Also, notice the semicolon at the end of the statement; every statement in rust needs to have a semicolon ending it.
To compile this, open your terminal in your main
rust
folder which containssrc
andbuild
folders.$ rustc src/main.rs -o build/main
This command should compile your
main.rs
file into amain
executable in build folder.rustc
is our compiler, followed by path of ourmain.rs
file followed by-o
flag which requires a path of our output file.After running this command, my direcory now looks like
rust ├── build │ └── main └── src └── main.rs
Finally to run the compiled file
$ ./build/main
Our Output
$ rustc src/main.rs -o build/main $ ./build/main Hello World
Congrats! You just created your first rust program
Variables and their types
Think of variables like a pocket, they store something for you. Variables have different types, for example there can be a variable for storing integers, there can be one for storing characters, etc.
Primitive variable types
# Integers: numbers
Signed
- u8: [0 - 255]
- u16: [0 - 65535]
- u32: [0 - 4294967295]
- u64: [0 - 18446744073709551615]
- u128: [0 - 340282366920938463463374607431768211455]
Unsigned
- i8: [-128 - 127]
- i16: [-32768 - 32767]
- i32: [-2147483648 - 2147483647]
- i64: [-9223372036854775808 - 9223372036854775807]
- i128: [-170141183460469231731687303715884105728 - 170141183460469231731687303715884105727]
# Floats: decimal numbers
- f32: single precision
- f64: double precision
# Boolean: true or false
- bool
# Character: single digit, alphabet, unicode
- char
# Tuples
# Arrays
# Strings: a group of unicode characters
- str
Default integer type is i32
and default float type is f64
.
Now back to our main.rs
file. Let's create a few variables inside our main
function.
To declare a variable, we use let
keyword followed by the variable name.
let age = 20;
let name = "Raahim";
let programmer = true;
let is_true = 150 == 100;
let face = '\u{1F600}';
println!("{} {} {} {} {}", name, age, programmer, is_true, face);
Our Output
$ rustc src/main.rs -o build/main
$ ./build/main
Hello World
Raahim 20 true false 😀
{}
is a placeholder and gets replaced by the variables we provide println
Let's try to change our variables
let age = 20;
let name = "Raahim";
let programmer = true;
let is_true = 150 == 100;
let face = '\u{1F600}';
println!("{} {} {} {} {}", name, age, programmer, is_true, face);
age = 21;
Compiling this gives us an error
$ rustc src/main.rs -o build/main
$ ./build/main
error[E0384]: cannot assign twice to immutable variable `age`
--> src/main.rs:11:3
|
3 | let age = 20;
| ---
| |
| first assignment to `age`
| help: consider making this binding mutable: `mut age`
...
11 | age = 21;
| ^^^^^^^^ cannot assign twice to immutable variable
error: aborting due to previous error; 1 warning emitted
For more information about this error, try `rustc --explain E0384`.
That leads us to our next topic
Mutable Variables
By default, all variables declared are immutable, meaning they can't be reassigned.
In order to make our variables mutable, we need to use the keyword mut
Let's change our age
variable to mutable
let mut age = 20;
age = 21;
This won't give us an error now.
What if we wanted to change these types? So far rust has automatically detected our variable types but sometimes we may require a different type to work with
Explicit Type Annotation
To explicitly tell rust the type of our variable, we have to follow our variable name with colon and then provide the type of our variable
For example
// main.rs
let age: u8 = 20;
let pi: f32 = 3.14;
You should only use f32 when you absolutely need it, like when you need to save extra memory or such
Declare multiple variables at the same time
Sometimes you want to declare variables in the same line, similar to python
, like my name and age, I can do the following
let (name, age) = ("Raahim", 20);
Constants
Constants are variables that cannot be changed.
To declare constants, we use const
instead of let
and the name of the constants should be all UPPERCASE
Like following
const ID: i32 = 1;
So what's the difference between a normal variable that's immutable by default, and a constant?
There are two differences
- Constants need to have a datatype explicitly
- Constants need to be intialized during declaration. This means that they need to have a value where they are getting declared. In comparison to normal variables, you can declare them without any value and initialize them later on.
println
, print
and text formatting
We've already used println
, it's used to print a line to the terminal. We've also used it to print variables. Now let's look into print and various ways of formatting our texts
println
println!("Hello Hashnode");
print!("Hello Hashnode\n");
The difference between println
and print
is that, println
automatically adds the line terminator (\n
) whereas print
does not add it at the end.
I'll be using println
throughout this guide
Arguments
To print our variables, we can use {}
as placeholder and pass the variables to println
or print
let name = "Raahim";
let age = 20;
const PI: f64 = 3.14;
println!("Hey, my name is {}, I am {} and Pi is {}", name, age, pi);
This gives us
$ rustc src/main.rs -o build/main && ./build/main
Hey, my name is Raahim, I am 20 and Pi is 3.14
Positional Arguments
To print our variables on specific positions in our statement we do
let name = "Raahim";
let age = 20;
const PI: f64 = 3.14;
println!("Hey, Pi is {2}, I am {1} and my name is {0}", name, age, pi);
That will print
$ rustc src/main.rs -o build/main && ./build/main
Hey, Pi is 3.14, I am 20 and my name is Raahim
Now let's go over what we actually did here
name
is our first argument inprintln
so it's at 0 index.age
is our second argument inprintln
so it's at 1 index.PI
is our third argument inprintln
so it's at 2 index.
Notice how each argument's index is one number behind their position? That's because indices in Rust begin at 0 instead of 1
So in order to change the way our variables are formatted, we just write the index of our variable in between braces {}
println!("Hey, Pi is {2}, I am {1} and my name is {0}", name, age, pi);
Named Arguments
Instead of using indices, we can use names for our arguments
let name = "Raahim";
let age = 20;
const PI: f64 = 3.14;
println!("Hey, Pi is {pi}, I am {myName} and my name is {myAge}", myName=name, myAge=age, pi=PI);
We can also use the variable names as is for positions
let name = "Raahim";
let age = 20;
const PI: f64 = 3.14;
println!("Hey, Pi is {PI}, I am {name} and my name is {age}");
Placeholder traits
println
and print
can convert numbers from decimal to binary, octal or hex on it's own using placeholder traits
println!("Binary: {:b} Hex: {:x} Octal: {:o}", 5, 10, 10);
$ rustc src/main.rs -o build/main && ./build/main
Binary: 101 Hex: a Octal: 12
Debug trait
Some variables/data can't be formatted, so to print them to the terminal for debugging, we use {:?}
println!("{:?}", (5, "hello", true));
$ rustc src/main.rs -o build/main && ./build/main
(5, "hello", true)
Operators
Arithmetic
These operators are used for mathematical operations
Addition +
+
adds two operands together.
let a = 10 + 15;
// 25
Subtraction -
-
subtracts second operand from first operand.
let a = 10 - 15;
// -5
Multiplication *
*
multiplies two operands together.
let a = 2 * 3;
// 6
Division /
/
divides two operands together.
let a = 15 / 3;
// 5
Modulo %
%
returns remainder of division between two operands.
let mut a = 15 % 2;
// 1
a = 33 % 5;
// 5
a = 10 % 2;
// 0
Comparison Operators
These operators are used to compare values, they always return a boolean, either true
or false
.
Equal to ==
==
is used to compare whether both operands are equal
let a = 10;
let b = 11;
let c = a == b;
// false
let a = 10;
let b = 10;
let c = a == b;
// true
Greater than >
>
used to compare whether first operand is greater than second operator
let age = 15;
let is_eligible = age > 17;
// false
let age = 26;
let is_eligible = age > 17;
// true
Less than <
<
used to compare whether first operand is smaller than second operator
let age = 15;
let is_minor = age < 18;
// true
let age = 26;
let is_minor = age < 18;
// false
Greater than or equal to >=
>=
used to compare whether first operand is greater than second operator, it's a mixture of >
and ==
let age = 18;
let is_eligible = age >= 18;
// true
Lesser than or equal to <=
<=
used to compare whether first operand is greater than second operator, it's a mixture of <
and ==
let age = 17;
let is_minor = age <= 17;
// true
Not equal to !=
!=
checks if first operand is not equal to second operator
let name = "Raahim";
let is_blocked = name != "Raahim";
// false
Logical Operators
These operators also return either true
or false
. These operators combine two or more conditions together.
AND &&
&&
returns true
if both operands are true
let a = true;
let b = false;
let c = a && b;
// false
let a = true;
let b = true;
let c = a && b;
// true
OR ||
||
returns true
if either of the operands are true
. It returns false
only if both operands are false
let a = true;
let b = false;
let c = a || b;
// true
let a = false;
let b = false;
let c = a || b;
// false
NOT !
!
returns inverse of the operand. true
is converted to false
and vice versa
let a = false;
let b = !a;
// true
Compound Assignment Operators
These operators manipulate the value and then assign the value in the same statement.
Add and Assign +=
+=
adds the number with the variable and assigns the variable with the new value
let mut a = 1;
a += 1;
// 2
The above code basically is the same as
let mut a = 1;
a = a + 1;
// 2
Subtract and Assign -=
-=
subtracts the number with the variable and assigns the variable with the new value
let mut a = 1;
a -= 1;
// 0
Multiply and Assign -=
*=
multiplies the number with the variable and assigns the variable with the new value
let mut a = 3;
a *= 2;
// 6
Divide and Assign /=
/=
divides the number with the variable and assigns the variable with the new value
let mut a = 4;
a /= 2;
// 2
I've not included bitwise operators here as they may get too confusing for newcomers.
Flow Control
Flow control means, controlling the order in which our code executes.
If
if
runs the code only if the condition provided to it is true
let is_admin = true;
println!("User logged in");
if is_admin == true {
println!("User is admin");
}
Output
$ rustc src/main.rs -o build/main && ./build/main
User logged in
User is admin
What if, we change our variable from true to false
let is_admin = false;
We get
$ rustc src/main.rs -o build/main && ./build/main
User logged in
The println
statement inside our if block didn't get printed, that's because our if statement got false.
Else
else
runs when our if
statement is not true.
let is_admin = false;
if is_admin == true {
println!("User is admin");
} else {
println!("User is not admin");
}
$ rustc src/main.rs -o build/main && ./build/main
User is not admin
Else If
We can have multiple if conditions chained together
let is_admin = false;
let is_mod = true;
if is_admin == true {
println!("User is admin");
} else if is_mod == true {
println!("User is mod");
} else {
println!("User is neither admin nor mod");
}
$ rustc src/main.rs -o build/main && ./build/main
User is mod
Shorthand If
We can use this to assign a variable some value depending on the if condition
let age = 18;
let is_eligible: bool = if age >= 21 { true } else { false };
// false
Match
match
statement is similar to having multiple else if statements
If you're coming from languages like C, C++, Java. You might be familiar with switch
statement, this is essentially a switch
statement.
It uses the keyword match
followed by variable name and then braces {}
which encapsulate the conditions and actions.
let role = "admin";
match role {
"admin" => println!("The user is admin!"),
"mod" => println!("The user is mod!"),
"guest" => println!("The user needs to login"),
_ => println!("Unknown user"),
}
$ rustc src/main.rs -o build/main && ./build/main
The user is admin!
If we were to change role
variable, it would change the output of match
.
Underscore _
is used for fallback conditions. (Similar to else
)
Another neat feature rust has over other languages is that it can combine cases together
for example
match role {
"owner" | "admin" => println!("The user is admin!"),
"mod" => println!("The user is mod!"),
"guest" => println!("The user needs to login"),
_ => println!("Unknown user"),
}
We can also provide a range to our match
let age = 17;
match age {
13..=19 => println!("Teenagers"),
_ => println!("Other age")
}
Loops
Loops are code blocks that keep on repeating
In rust, we have 3 types of loops
- loop
- for
- while
Loop
loop
is an infinite loop. It runs forever until we explicitly tell it to stop.
let mut count = 0;
loop {
count += 1;
println!("{count}");
if count >= 5 {
break;
}
}
$ rustc src/main.rs -o build/main && ./build/main
1
2
3
4
5
Let's talk about what we did here,
We first declared a mutable
count
variable and initialized it to 0.let mut count = 0;
We started our loop
loop {
On the start of our loop we added 1 to our variable
count += 1;
We printed our
count
println!("{count}");
We checked if our count is greater than or equal to 5, if it is, we
break
out of the loop, otherwise our loop goes back to the start and repeats from step 3.if count >= 5 { break; }
For
For loops have a range, they start at a specific number we provide them and end before a specific number we provide them.
for x in 0..5 {
println!("{x}");
}
$ rustc src/main.rs -o build/main && ./build/main
0
1
2
3
4
Let's go through our code,
We started our loop with range 0 to 5, which will get stored in
x
variablefor x in 0..5 {
We print our
x
variableprintln!("{x}");
and after that the loop goes back to the start, now what comes after 0? 1. So
x
turns into 1, and then it prints out to screen. This happens until we reach 4. The loop breaks 1 number before our final number 5.
While
While loops run until a condition is true
Let's do something exciting for this loop
let mut count = 0;
while count <= 100 {
if count % 15 == 0 {
println!("fizzbuzz");
}
else if count % 3 == 0{
println!("fizz");
}
else if count % 5 == 0 {
println!("buzz");
}
else {
println!("{count}");
}
count += 1;
}
$ rustc src/main.rs -o build/main && ./build/main
fizzbuzz
1
2
fizz
4
...
...
...
97
98
fizz
buzz
This is fizzbuzz
, a simple programming task. In this task, we run a loop from 1 to 100, we print fizz
if the number is divisible by 3, buzz
if the number is divisible by 5, fizzbuzz
if the number is divisible by both 3 and 5 and if neither of the above conditions are satisfied, it should just print the loop number.
That's exactly what's happening in our while
loop.
First we declared
count
variable to keep track of our loop iteration; we made it mutable as well.let mut count = 0;
Then we started our while loop with the condition that it will keep running as long as our count is less than or equal to 100
while count <= 100 {
- Then comes our if statements
if count % 15 == 0 { println!("fizzbuzz"); } else if count % 3 == 0{ println!("fizz"); } else if count % 5 == 0 { println!("buzz"); } else { println!("{count}"); }
Finally we add 1 to our count variable
count += 1;
After this, it goes back to start of the loop, if count is less than or equal to 100, the loop will run again, otherwise it will exit out of the loop.
Strings
Let's talk about strings, we have 2 types of strings in Rust.
str
and String
str
is a primitive datatype, it has fixed length and is immutable.
String
is a heap allocated data structure which is growable, meaning it's length can increase unlike str
Str
We can make string variables like this
let name = "Hello There";
let name: &str = "Hello There";
Both of the above are valid, they both make str
type variable.
String
To make String
let hello = String::from("Hello");
let hello: String = String::from("Hello");
Both are correct but usually the first way is used as it already has String
in the value.
String
also have a lot of built in functions.
push
Let's say you want to append to the end of the string
let mut hello = String::from("Hello");
hello.push(" world");
println!("{hello}");
$ rustc src/main.rs -o build/main && ./build/main
Hello world
len
len
is used to get the length of the string. Length of the string is the number of characters it has
let hello = String::from("Hello");
println!("{}", hello.len());
$ rustc src/main.rs -o build/main && ./build/main
5
Why did we get 5? We got 5 because "Hello" has 5 characters.
Note: Characters can be alphabets, numbers, spaces, symbols, etc
capacity
capacity
gives us the size of the string in bytes
let hello = String::from("Hello");
println!("{}", hello.capacity());
$ rustc src/main.rs -o build/main && ./build/main
5
is_empty
is_empty
, sounds straight forward, it returns a boolean, it tells us if our string variable is empty or not
let var = String::new();
println!("{}", var.is_empty());
let another_var = String::from("Hello my friend");
println!("{}", another_var.is_empty());
$ rustc src/main.rs -o build/main && ./build/main
true
false
contains
This returns a boolean. It tells us if a word or a piece of string exists in our variable or not.
let hello = String::from("Hello my friend");
println!("{}", hello.contains("my"));
$ rustc src/main.rs -o build/main && ./build/main
true
replace
We can also replace a specific piece of our string with a different string.
let hello = String::from("Hello my friend");
println!("{}", hello.replace("my", "your"));
$ rustc src/main.rs -o build/main && ./build/main
Hello your friend
split_whitespace
It splits the string by whitespace. A whitespace can be defined as a space, a tab, etc.
It returns in iterator, which can't be formatted by our default formatting trait {}
, so instead we'll be using the debug trait {:?}
let hello = String::from("Hello my friend");
println!("{:?}", hello.split_whitespace());
$ rustc src/main.rs -o build/main && ./build/main
SplitWhitespace { inner: Filter { iter: Split(SplitInternal { start: 0, end: 15, matcher: CharPredicateSearcher { haystack: "Hello my friend", char_indices: CharIndices { front_offset: 0, iter: Chars(['H', 'e', 'l', 'l', 'o', ' ', 'm', 'y', ' ', 'f', 'r', 'i', 'e', 'n', 'd']) } }, allow_trailing_empty: true, finished: false }) } }
What's all this? This is a bit messy. It's all the information that variable contains right now.
Since it's an iterator, we can use it in a for loop.
If you remember, we gave a range to our for loop last time.
This time, let's give it our iterator
let hello = String::from("Hello my friend");
for word in hello.split_whitespace() {
println!("{word}");
}
$ rustc src/main.rs -o build/main && ./build/main
Hello
my
friend
Basically, our string gets split into an iterator which contains a list of our words that were separated by a space.
We printed it word by word using the for loop.
with_capacity
We can also make a new string with our own capacity
let mut s = String::with_capacity(10);
println!("Capacity: {}", s.capacity());
s.push('a');
s.push('b');
println!("{s}");
$ rustc src/main.rs -o build/main && ./build/main
Capacity: 10
ab
Tuples
A tuple is a grouped value. It can have different types of data in it. A tuple can contain at max 12 elements.
Tuples can be created using parenthesis ()
.
To access the data in a tuple, we use indices. Like we learnt earlier about indices, they start at 0.
Let's look at an example
let person: (&str, &str, u8) = ("Raahim", "Pakistan", 20);
println!("{} is from {} and is {}", person.0, person.1, person.2);
$ rustc src/main.rs -o build/main && ./build/main
Raahim is from Pakistan and is 20
Arrays
Arrays can store multiple data of same data types. Their length are fixed. They are stack allocated data structures.
Let's create an array of single digit positive even numbers.
let mut numbers: [i32; 4] = [2, 4, 6, 8];
The datatype of our array is i32
and it contains 4
elements. Our elements are 2, 4, 6, 8
Since our array is mutable using mut
keyword, we can change it's data.
To access an array, we use indices. First element is stored at 0 index, second at 1 index and last index is stored at n - 1 index, n being the length of the array.
numbers[1] = -4;
We can use loops to iterate over our arrays.
for number in numbers {
println!("{}", number);
}
$ rustc src/main.rs -o build/main && ./build/main
2
-4
6
8
len
We can get the length of an array using len
println!("{}", numbers.len());
Slicing array
We can slice an array as following
println!("{:?}", &numbers[2..4]);
$ rustc src/main.rs -o build/main && ./build/main
[6, 8]
2..4
means, our sliced array will start at index 2, and end before index 4.
Basically, 2 is included, 4 is not included, the one previous to 4 is included, which is 3.
Vectors
Vectors are resizable arrays. Meaning, their length can be increased or decreased.
We can create vectors using vec!
macro or using Vec
as datatype.
For example
let even_numbers: Vec<&str>;
let people = vec!["John", "Jane", "Doe"];
Accessing elements in a vector is the same as in an array.
println!("{}", people[1]);
// Jane
Push - Insert into vector
Push is a function in vector, we can use it to insert data into our vector.
let mut people = vec!["John", "Jane", "Doe"];
people.push("Person");
println!("{:?}", people);
println!("{}", people[2]);
$ rustc src/main.rs -o build/main && ./build/main
["John", "Jane", "Doe", "Person"]
Doe
remove
remove
deletes a value at the given index.
let mut people = vec!["John", "Jane", "Doe"];
people.remove(1);
println!("{:?}", people);
$ rustc src/main.rs -o build/main && ./build/main
["John", "Doe"]
len
Similar to array, returns the length of the vector
vector.len()
pop
Pop removes the last element in the vector and returns it as well
let mut people = vec!["John", "Jane", "Doe"];
println!("{:?}", people.pop());
$ rustc src/main.rs -o build/main && ./build/main
Some("Doe")
loops with vectors
Unlike arrays, with vectors we need to use methods in order for loops to work.
Let's take a look at a simple example
let people = vec!["John", "Jane", "Doe"];
for person in people.iter() {
println!("{}", person);
}
$ rustc src/main.rs -o build/main && ./build/main
John
Jane
Doe
Notice how we're using iter
method. Similarly, if you have a mutable vector and you want to change the values in the for loop as well, we can use iter_mut
Functions
Functions are reusable code blocks. We can declare a function and then recall it as many times we want without rewriting all the code.
They're used to make our code more reusable.
You're already introduced to main
function.
Just to recap, every rust program needs an entry point function named main
.
It's usually in our main.rs
file.
fn main() {
...
}
So far we've worked inside our main
function but we'll make our own functions outside main
function.
Now let's create our own function.
fn main() {
...
}
fn hello() {
println!("Hello World!");
}
In the above example, I created a hello
function outside main
function.
But how do we use it now?
To call a function, we simple use it with parenthesis. For our hello
function, we'd call it like hello()
Let's try it
fn main() {
hello();
}
fn hello() {
println!("Hello World!");
}
$ rustc src/main.rs -o build/main && ./build/main
Hello World!
Notice how I called my hello
function inside main
function? Our function was declared outside main
but we need to call it inside main
or some other function.
The benefit of this function is that I can call it as many times as I want.
hello();
hello();
hello();
hello();
$ rustc src/main.rs -o build/main && ./build/main
Hello World!
Hello World!
Hello World!
Hello World!
We can also pass arguments to our functions. Arguments are our variables or values that we want to use inside our functions.
Let's create an add function and let's pass 2 numbers to it.
fn main() {
add(1, 3);
add(15, 87);
}
fn add(x: i32, y: i32) {
println!("{x} + {y} = {}", x + y);
}
$ rustc src/main.rs -o build/main && ./build/main
1 + 3 = 4
15 + 87 = 102
In this function we printed our values.
Alternatively, we can return our values from our functions to reuse later; we can store them in variables;
In order to do that, we have to define our return type of our function after our paraments and we have to use return
statement.
fn main() {
let result = add(1, 3);
}
fn add(x: i32, y: i32) -> i32 {
return x + y;
}
Now we learnt in the start that every statement in Rust needs to end with a semicplon ;
, but when we want to return a value in a function, we can exclude the semi colon and return
keyword
Let me show you how, let's change our add function
fn add(x: i32, y: i32) -> i32 {
x + y
}
This add function is the same as the previous one. The only difference is now we're not using return
and ;
.
Closures | Anonymous Functions |
Closures in rust are anonymous functions that we can save in variables or pass to other functions
Let's convert our add function into a closer
let add = |n1: i32, n2: i32| n1 + n2;
println!("{}", add(5, 3));
$ rustc src/main.rs -o build/main && ./build/main
8
Structs
Earlier, we talked about tuples
; tuples
are a type of Struct.
If you're coming from object-oriented languages, you may be familiar with classes. Structs are somewhat similar to classes.
If you're coming from C, you may be familiar with struct
in C. Rust Struct is C styled.
Structs with named fields
To create a struct, we use the struct
keyword, followed by a name, and then braces which encapsulate named fields and their datatypes.
struct Person {
first_name: String,
last_name: String
}
How do we use this though? Let's create a variable with this struct.
let john = Person {
first_name: "John".to_string(),
last_name: "Doe".to_string()
};
Our fields are of String
type, but we're assigning them str
, that's why we called to_string
methods, these are already created for us by the wonderful people working at rust.
Unlike tuples
where we needed to use indices, here we can just access our struct values by their field name
println!("Full name: {} {}", john.first_name, john.last_name);
Implementation
Implentation is basically adding functions to our structures. Just before last example, I mentioned that structures are somewhat similar to classes, what do classes have?
They have properties (variable) and methods (functions). What do our structures have? Named fields which we can consider as properties and through implementation, we can also add methods to our structures.
impl
keyword is used to make an implementation. It needs to have the same name as the struct you're implementing.
Let's take a look at how,
in our main function
struct Color {
r: u8,
g: u8,
b: u8
}
impl Color {
fn new(r: u8, g: u8, b: u8) -> Color {
Color {
r,
g,
b
}
}
fn to_tuple(&self) -> (u8, u8, u8) {
(self.r, self.g, self.b)
}
}
let cyan = Color::new(0, 255, 255);
println!("R: {}, G: {}, B: {}", cyan.r, cyan.g, cyan.b);
println!("{:?}", cyan.to_tuple());
$ rustc src/main.rs -o build/main && ./build/main
R: 0, G: 255, B: 255
(0, 255, 255)
So what happened here?
First we created our
Color
structurestruct Color { r: u8, g: u8, b: u8 }
Then we created our implementation using
impl
impl Color { ... }
In our implementation, we created
new
function. That's used to create a new instance of our structure.fn new(r: u8, g: u8, b: u8) -> Color { Color { r, g, b } }
It returns
Color
Then we created a
to_tuples
, which just returns our structure as a tuplefn to_tuple(&self) -> (u8, u8, u8) { (self.r, self.g, self.b) }
Finally, we create our variable and print out the values
let cyan = Color::new(0, 255, 255); println!("R: {}, G: {}, B: {}", cyan.r, cyan.g, cyan.b); println!("{:?}", cyan.to_tuple());
Enums
Enums can be considered as custom data types.
Let's look at a few situations where enums may be used.
In games for instance, a player may be dead or alive. We can create an enum for that.
enum PlayerState {
Dead,
Alive
}
Or for player movement, a player can move in 4 directions, forward, backward, right and left.
enum PlayerMovement {
Right,
Left,
Up,
Down
}
Or the team of our player, is it in Red team, or Blue team, or is it a spectator?
enum PlayerTeam {
Red,
Blue,
Spectator
}
We can use it for days as well
enum Days {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
}
Now that we know how to define an enum, let's create some variables.
let today = Days::Tuesday;
Our Days
enum has seven possible values. If we create a variable with Days
datatype, it can be assigned one of those seven values.
Similarly for PlayerState
we can do
let alive = PlayerState::Alive;
let dead = PlayerState::Dead;
Comments
Comments are a piece of code that get ignored by our compiler. We can use comments to leave messages in between our code for our future self or even other developers, explaining our code, so that it's easier to read our code when we pick it up after a few weeks or when we give it to someone else.
Comments can also be used to temporarily disabled a piece of code.
Single line comments
Single line comments can be made using double slash //
// This is a single lined comment
// Name variable
let my_name: &str = "Raahim";
// Don't need admin variable for now
// let admin: &str = "John Doe";
// If it's Raahim, do something
if my_name == "Raahim" {
...
}
// If it's not Raahim, do something else
else {
...
}
Double line comments
If we have multiple lines that we need to comment, it can be cumbersome to put '//' behind every line.
To overcome this, we use /**/
/*
This is a
multi line
comment,
It can span many lines
*/
let mut count = 0;
while count <= 100 {
/*
if count % 15 == 0 {
println!("fizzbuzz");
}
else if count % 3 == 0{
println!("fizz");
}
else if count % 5 == 0 {
println!("buzz");
}
else {
println!("{count}");
}
*/
println!("{count}");
count += 1;
}
In the above code, we commented out our if statements.
Generics
Generics are used when we want to create the same function for different data types.
For example, an add
function which adds two numbers together, you may want to create it for all types of numbers, u8
, i8
, u16
... i128
, f32
, f64
. That's gonna be a lot of functions.
For functions like these, we use generics.
Let's create a generic function
fn main() {
println!("{}", function("Hello there"));
println!("{}", function(1));
println!("{}", function(3.14));
println!("{}", function(-100));
println!("{}", function('f'));
println!("{}", function(true));
println!("{:?}", function([1, 2, 3]));
println!("{:?}", function((4, 5, 6)));
println!("{:?}", function(vec![0, 2, 4, 6, 8]));
}
fn function<T>(var: T) -> T {
var
}
$ rustc src/main.rs -o build/main && ./build/main
Hello there
1
3.14
-100
f
true
[1, 2, 3]
(4, 5, 6)
[0, 2, 4, 6, 8]
Let's see what we did here
We created a function named
function
and besides the name, we added<T>
Basically
<T>
is a placeholder for datatypes. It can be an integer, a float, boolean, any datatype, even custom datatypes such as structures and enums.We also have an argument of the function with datatype
T
and our function also returns a value with datatypeT
fn function<T>(var: T) -> T { ... }
In the function we simply returned our argument
fn function<T>(var: T) -> T { var }
In our main functions, we had our print statements, we used many different data types for arguments in our function.
println!("{}", function("Hello there")); println!("{}", function(1)); println!("{}", function(3.14)); println!("{}", function(-100)); println!("{}", function('f')); println!("{}", function(true)); println!("{:?}", function([1, 2, 3])); println!("{:?}", function((4, 5, 6))); println!("{:?}", function(vec![0, 2, 4, 6, 8]));
Now let's create an add function
fn add<T>(one: T, two: T) -> T {
one + two
}
If we compile this, we get an error
$ rustc src/main.rs -o build/main && ./build/main
error[E0369]: cannot add `T` to `T`
--> src/main.rs:18:7
|
18 | one + two
| --- ^ --- T
| |
| T
|
help: consider restricting type parameter `T`
|
17 | fn add<T: std::ops::Add<Output = T>>(one: T, two: T) -> T {
| +++++++++++++++++++++++++++
error: aborting due to previous error
For more information about this error, try `rustc --explain E0369`.
What's going on here?
The thing is, our datatype T
can be a number, string, even custom datatypes like structures as we've established before but this comes with it's own problems. We can't add our custom datatypes together, in that case +
will not work. So to avoid that later on, it gives us an error right now.
To fix that, we need to give a trait to our parameter type T
.
If we read our error message, it also gives us a hint of what to do
help: consider restricting type parameter `T`
|
17 | fn add<T: std::ops::Add<Output = T>>(one: T, two: T) -> T {
| +++++++++++++++++++++++++++
It even tells us the line number at which the error occurred, in my case it's line 17.
So let's add the trait std::ops::Add<Output = T>
to our T
parameter datatype.
fn add<T: std::ops::Add<Output = T>>(one: T, two: T) -> T {
one + two
}
If we compile this now, it won't give us an error.
Let's use it
println!("{}", add(1, 2));
println!("{}", add(5, 10));
println!("{}", add(3.12, 0.02));
It should just give us
3
15
3.14
Similarly for subtraction we would use std::ops::Sub<Output = T>
,
for multiplication std::ops::Mul<Output = T>
,
for division std::ops::Div<Output = T>
You can find more information at Rust official website
Cargo
Cargo is a rust's build system and package manager. It's used to install libraries, handle your project and compile your code.
It should be automatically installed in your system if have rustc
installed.
Let's check
$ cargo --version
cargo 1.58.0 (f01b232bc 2022-01-19)
Your cargo version may be a bit different but that's okay
Creating a project with cargo
To create a project we can simply run the command
$ cargo new <project_name>
You need to replace <project_name>
with the name of your project.
$ cargo new hello_universe
Created binary (application) `hello_universe` package
Let's cd
into our new project
$ cd hello_universe
Your directory should look like
.
├── Cargo.toml
└── src
└── main.rs
1 directory, 2 files
Cargo made a project folder for us, added Cargo.toml
to it, created a src
folder inside it and added main.rs
to our src
folder.
Cargo.toml
This is the config file for our project.
For me it looks like
[package]
name = "hello_universe"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
The first section is [package]
. It contains information about our project.
The second section is [dependencies]
. It contains the information of other libraries that our project requires.
main.rs
file
Opening our main.rs
file, we see that cargo has already created a main function for us
fn main() {
println!("Hello, world!");
}
Compiling our project and running it
So far we've used the command rustc
to compile our program, but now we'll use cargo
to handle it fo us.
$ cargo build
If we run that, we get an output
$ cargo build
Compiling hello_universe v0.1.0 (/home/user/rust/hello_universe)
Finished dev [unoptimized + debuginfo] target(s) in 0.28s
This command creates an executable file in _target/debug/hellouniverse
We can run that file by either calling it in terminal
$ ./target/debug/hello_universe
Hello, world!
Or we can use cargo
to run it for us
$ cargo run
Running that command gives us
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/hello_universe`
Hello, world!
We can also only use cargo run
to compile and run our file instead of first running build
and then run
Let's change our print statement in main.rs
to
println!("Hello, Universe!");
If we now run cargo run
we get
$ cargo run
Compiling hello_universe v0.1.0 (/home/user/rust/hello_universe)
Finished dev [unoptimized + debuginfo] target(s) in 0.25s
Running `target/debug/hello_universe`
Hello, Universe!
Notice how it compiled our file and then ran it in the same command.
Cargo check
If you're like me, you often save and compile your program to check for errors. The thing with cargo build
is that even though it compiles our file, it creates an executable, which makes it a bit slower, at small projects it's not noticeable, but it becomes noticeable as our project goes.
In that case we use cargo check
, this command compiles our project and tells us of errors, but it doesn't create an executable so it's faster than cargo build
$ cargo check
Checking hello_universe v0.1.0 (/home/user/rust/hello_universe)
Finished dev [unoptimized + debuginfo] target(s) in 0.14s
Building for release
Once you're ready to release your project, there's --release
flag that optimizes the code and removes debugging flags.
$ cargo build --release
Installing dependencies
Later on, you may need to install libraries and dependencies in order to help with you projects
For this, we'll need to list our dependencies in our Cargo.toml
file.
When you open Cargo.toml
It should have [dependencies]
. You have to list your dependencies under this heading.
If you don't have this heading, just add it at the bottom of Cargo.toml
Let's add time
in our dependencies
[package]
name = "hello_universe"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
time = "*"
Asterisk * means, cargo will install the latest possible version for us.
Let's now run cargo build
and it'll automatically install the dependencies and build or program.
$ cargo build
Updating crates.io index
Downloaded num_threads v0.1.6
Downloaded time v0.3.9
Downloaded libc v0.2.126
Downloaded 3 crates (679.2 KB) in 3.39s
Finished dev [unoptimized + debuginfo] target(s) in 5m 05s
Reading the output, we can see it installed time
dependency. It's version is v0.3.9
Now that we've installed it, how do we use it in our program?
That leads us to our next topic
use
crate
Our dependencies and packages are called crates. To use them in our project, we use the use
keyword.
Let's take a look at an example
use time;
fn main() {
let date = time::Date::from_calendar_date(2022, time::Month::May, 26);
println!("{:?}", date.unwrap());
}
$ cargo run
Date { year: 2022, ordinal: 146 }
We added
time
create usinguse
keyword.use time;
In our
time
crate, we haveDate
struct, and inside ourDate
struct we have a function namedfrom_calendar_date
, we're calling that function and creating a variablelet date = time::Date::from_calendar_date(2022, time::Month::May, 26);
Finally, we're printing our result after unwrapping it.
println!("{:?}", date.unwrap());
pub
and mod
pub
is used when we want to make our enums, structs and functions be externally available.
By externally available, it means that other files will be able to access these.
To use these in other files, we use mod
in those external files.
Let's take a look at an example
// functions.rs
pub fn hello_world() {
println!("Hello, World!");
}
// main.rs
mod functions; // name of the file without .rs
fn main() {
functions::hello_world();
}
$ cargo run
Hello, World!
Both files main.rs
and functions.rs
are in the same directory.
Final Steps
Now that you're familiar with the language, here are some resources to help you further
- Official Rust Website
- The Rust Programming Language, book for reference and syntax
- crates.io, rust community crate registry
Some things you can try in Rust
- Make a website using Rust and WebAssembly
- Try out Piston, a game engine
- Make a SPA in Rust
- Some more project ideas