Welcome to Rust!
About this book:
Important notice #0:
This book is meant to be for people new to programming and want to learn rust. Despite being an avid rust lover, I will advise you to not learn rust. In fact, Rust has so many complex concepts that take even the most advanced developers time to truly understand. My advice would be to learn the basics of python and / or JavaScript, and then come back to rust. However, if you are stubborn like many developers (and me), then you are welcome to use this book and try to learn rust.
Important notice #1:
This book has been simplified and some deeper explanations has been skipped. My advice would be to use this book, but then once you have the basics down, take on the challenge of using the official book which should be significantly easier to understand as they use more technological terms in comparison to this book.
Just some boring stuff about me:
This book, is not affiliated with any company / foundation / organisation. It is currently a solo project by a 17-year-old rust developer, who had trouble learning rust from the book due to its extreme amounts of terminology used, and wants to help others learn rust as well. If you are a person from the rust foundation, then I would really appreciate it if you could try and voice this project out for me.
Installation / Alternatives
I would recommend installing Rust which will also install cargo. However, you can also use the rust playground for the first few chapters. However, after a certain point of time (chapter 4’s modules section) It would be required to install Rust to make your life easier.
Use this with VSCode or any IDE / Code editor of your choice. I recommend VSCode or Zed
Setup
Creating a new project happens via the command line.
I’ve made a step by step guide on how I recommend you to create a new project.
- Create a folder to store your projects with your file explorer / finder
- Launch VSCode (or Zed), then click on File > Open Folder > Select the folder you just created
- Then open the terminal with CMD + ` (MacOS) or CTRL + ` (Windows / Linux)
- Here, run
cargo --versionto ensure that you have cargo installed. - If you do not have rust installed, you can install it from here
- Then run
cargo new hello_worldto create a new rust project called hello_world (You can change the name in the future according to what you want to call your project) - Then open the project in VSCode the same way you opened the projects folder (Step 2)
- Then run
cargo runto run your project
Recommended Additional Resources:
- Discord - This discord is filled with a bunch of smart hackers, (and me) and you will be able to ask any of your questions and get help with understanding various concepts with the help from others. However, they will only help you, if you are willing to help yourself, by using google if you can.
- Official Book - This is the official rust book. This current book, is a dumbed down version, with some of the advanced chapters skipped.
Chapter 1 - Hello, World!
Notes regarding the book
In many portions of the book, you may see statements like:
#![allow(unused)]
fn main() {
let ignore = 0;
}
It usually implies that the statment is inside a function (like fn main()) and
you can simply click the unhide button that appears when you hover over the code
snippet
Hello world is the first program that most programming languages teach beginners. Many tutorials will teach you how to “print” Hello, World! to the terminal / console as the first project
In rust, everything starts with a main function (annotated with fn).
Ignore what a function is for now. Just imagine a function as a container with things inside it. We will cover functions more in Chapter 4
This main function is the entry point of the program.
If you run the program, the main function will be executed.
You can use cargo to create a new project (cargo new project_name) and the below code will be preset in src/main.rs.
fn main() {
println!("Hello, world!");
}
Rust also has it’s unique macros which are somewhat similar to functions, so you can ignore them for now.
As of now, I do not plan on covering macros in this book so you might want to check out the official book after this book to find out more about macros.
Comments
Comments are used to explain the code. The compiler does not care about comments.
// means the rest of the line from this point on is a comment
/* and */ means everything between is to be ignored / is a comment
You can see how comments are written inside code in this example:
// This is a single line comment
/* This is a multi-line comment
It can span multiple lines */
fn main() {
println!(/*This comment gets ignored by the compiler*/"Hello, World");
// The above line is equivalent to the below line to the compiler
println!("Hello, World");
}
The rust community rarely uses multiline comments and usually resorts to single line comments more. This is because they are an idiomatic comment style. So please support the idiots by using inline comments.
Chapter 2 - Variables
Try to understand the below statement. It’s ok if you don’t get it, it’s explained afterwards
You set the variables mutability when you are declaring a variable
Let’s unpack the above statement
A variable is used to store data.
Its concept is similar to a box (but made with glass so that you can see whats inside it). Inside it, there is something, but it can be different based on different factors (Like the user’s OS or the time) In simple words, it can vary, hence why we call it a variable
In Rust, Variables can either be mutable or immutable.
This means that some variables are allowed to change while others are not.
Mutable means it can change, while immutable cannot be changed. Continuing our analogy from before, Immutable variables are the glass boxes and are sealed, so we can only look at what it is. Whereas a mutable variable is not sealed, allowing you to open it, change whats inside it, and put it back inside
Here is how to define variables, and make them mutable.
#![allow(unused)]
fn main() {
let mut mutable_variable = 0;
let immutable_variable = 0;
}
Declaring a variable is telling rust that when we use the letter
iwe mean the value stored insidei
Rust variables are made with the let keyword followed by their name By
default, variables are immutable. Making them mutable requires the mut keyword
The following code will not throw any errors if you try to run it.
#![allow(unused)]
fn main() {
let mut i = 0;
i = 2;
}
Try removing the mut from the code, and then run the code with cargo run
#![allow(unused)]
fn main() {
let i = 0;
i = 2;
}
What error does it give?
It should be similar to:
Compiling playground v0.0.1 (/playground)
error[E0384]: cannot assign twice to immutable variable `i`
--> src/main.rs:4:1
|
3 | let i = 0;
| - first assignment to `i`
4 | i = 2;
| ^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
3 | let mut i = 0;
| +++
For more information about this error, try `rustc --explain E0384`.
error: could not compile `playground` (bin "playground") due to 1 previous error
The error message: cannot assign twice to immutable variable i means that we
cannot give i a value twice (or in other words, cannot change its value)
Therefore, i is an immutable variable. Notice how we didn’t have to specify
that i is an immutable variable. That’s because by default, all variables are
considered immutable. This is a feature exclusive to rust.
If you have experience with other coding languages, you may wonder why Rust has this feature and the reason is simple. Some variables are meant to be the same throughout the program. And Rust is the teacher holding a stick waiting to make sure you don’t change something you aren’t meant to change in the future. There are some more advanced reasons that you can have a look at once you’re more experienced with Rust.
Type definitons
A variable in rust can only be of a certin type, or in other words, the glass box’s size is fixed, and cannot be changed.
Some types in rust are:
String&&str(These are different types but both are String types, which are used for storing text)Vec<T>(Ignore the<T>for now, just imagine it as saying that its a Vec of a type withTas a placholder) We will discuss this more later in this chapter.i8,i16,i32,i64, andi128. In simple terms, each of these types store a negative / postitive number with a limit of2^32as the max value if itsi32u8,u16,u32,u64andi128. This is the same as theiseries, but it can only store positive numbers.- The
fseries, which stores decimal numbers, like22.1, etc.
You specify a Variables type in the following manner:
#![allow(unused)]
fn main() {
let string: Type = SomeValueHere;
}
Let’s go through all the above mentioned types and their definitions.
#![allow(unused)]
fn main() {
let string_1: &str = "Something"; // &str
let string_2 = "This is also an &str"; // &str
let string_3: String = String::from("a String type is used to store text"); // String
let string_4 = "Another way to define a String".to_string();
}
Let’s unpack each line one by one.
- After telling rust that we are making a variable with
let, we go into saying thatstring_1should have a type of&str(“the shape of the box”) with: &strand then we “put something inside the box” with= "Something". - In the second line, we talk don’t tell Rust that we want a
&str. This is Rust automatically selecting the box shape, hence why these type’s aren’t needed most of the time. - In the third line, we see a new type called
String. This is slightly different from&strbut for now we will treat it as the same wherever possible. Defining aStringrequires us to either useString::from()or do:"&str here".to_string()which tells rust to convert a&strto aString
Using Variables
Variables can be used by their names. For example, when we are creating a
String we can make an &str into a string with .to_string(). Here’s an
example:
#![allow(unused)]
fn main() {
let example = "Rust is so Fun!!";
let example_as_a_string = example.to_string();
}
You can also do:
#![allow(unused)]
fn main() {
let another_example = String::from(example);
}
and it will work the exact same way.
Other variable types:
The i Series
#![allow(unused)]
fn main() {
let i_8: i8 = 1;
let i_16: i16 = -14;
let i_32 = 45; // Notice how we don't specify i32 here? i32 is the default for numbers in rust.
let i_64: i64 = -6493;
let i_128: i128 = -128231;
}
The u Serise:
#![allow(unused)]
fn main() {
let u_8: u8 = 1;
let u_16: u16 = 14;
let u_32: u32 = 45;
let u_64: u64 = 6493;
let u_128: u128 = 128231;
}
The f Series:
#![allow(unused)]
fn main() {
let f_8: f8 = 0.1;
let f_16: f16 = 0.1;
let f_32: f32 = 0.1;
let f_64 = 0.1; // Floating point types are automatically inferred as f64's, but you can also explicitly define them.
let f_128: f128 = 0.1;
}
Despite humanity having achieved amazing feats, accurate floating point mathematical operations are not one of them. The reason is kind of blurry to me as well, so here’s a YouTube Video you can use to understand why if you are interested. While the reason is not neccessary to know for now, I recommend knowing that floating point arithmetic is one the biggest weak points in computers.
Vec Data Type
Earlier in this chapter, I mentioned a Vec<T> where T is a placeholder for a
type. This is called a Vector or in other languages, called Array. In real life
Array and Vector do have a few differences, however for simplicity reasons,
you can consider them the same.
A Vector can be thought of as a “List” of items (You may also here people use
the word list as an alternative to arrays) It stores an unknown amount of
data. (The limit is how much your hardware supports)
In rust, there are a many ways to make a Vector, but I will show you only one. The other ways are more complex, and you would probably want to take a look at the book for that.
The most simple way to make a Vector is with vec![]
#![allow(unused)]
fn main() {
let odd_numbers = Vec![1,3,5,7,9];
}
Remember the
<T>? What if we were to specify a type for a vector?
#![allow(unused)]
fn main() {
let a_string = "some text";
let str_excl: Vec<&str> = vec![a_string, "Another element", "Another"];
}
This tells rust that we want a Vector that stores &str’s inside it.
Each value in the vector also has an index, which is a number thats used to access an element based on its position, such as 1,2,3… However, in programming, the first number is usually 0, and not 1. So accessing the first element of the Vector can be done as seen in the example below:
#![allow(unused)]
fn main() {
let vector = vec!["something", "something_else"];
let first_element = vector[0];
println!("{}", first_element);
}
In the next chapter, we will learn more about using the values inside a Vector with
forloops. I know this sounds like fruit loops, but you can’t eat these, and they don’t mix well with milk.
Chapter 3 - Control Flow
In programming, control flow usually means one of the following:
-
ifa condition is met, then we do something. -
else if- If the previous condition are not -
elsedo something else -
whilea certain condition is met, keep executing the code. -
forevery item inside a Vector, do something with the item. (For loops are not restricted to Vector’s as we will see later)
Let’s start with the if, else if and else statements.
Let’s quickly cover another data type called bool, or Boolean. A Boolean
stores just 2 possible values, true or false.
#![allow(unused)]
fn main() {
let yes = true;
let no: bool = false;
}
An if statement checks if something evaluates to true (a condition is met)
or false (a condition is not met)
An else if statement is a continuation to an if statement, which is like, if
the previous conditions are NOT met, but the condition in the else if is
met, then the code gets executed.
An else statement is for, when none of the conditions get met, then this code
gets reached, and executed.
Try playing around with the code below, and try to get all the conditions met.
#![allow(unused)]
fn main() {
let condition = true;
if condition {
println!("This condition is true.");
} else if !condition { // ! reverses the condition, AKA true becomes false and false becomes true
println!("The condition is not true");
} else {
println!("Can this statement be reached?");
}
}
Could you reach all the conditions? If you managed to reach the last condition… please be careful. You have probably been put under a watchlist. The last else statement was
If you notice, the last condition is impossible to reach. This is because we are
using a boolean for comparison directly, hence one of the first 2 conditions
will be met. So what’s the use of else if and else?
Let’s see another example, where we use strings instead of booleans. But how do
we use strings here? We use comparison operators. We will start with the basic
ones, == and !=.
#![allow(unused)]
fn main() {
let string_comparison = "input_2";
if string_comparison == "input_1" {
println!("Input");
} else if string_comparison != "input_2" {
println!("Neither input 1 or 2");
} else {
println!("It's Input 2");
}
}
In Rust, we have a better way for doing such comparisons. It’s called a match
statement.
Let’s try doing the same string comparison but with this match statement:
#![allow(unused)]
fn main() {
let string_comparison = "input_2";
match string_comparison {
"input_1" => println!("String comparison is input_1"),
"input_2" => println!("String comparison is input_2"),
_ => println!("String comparison is Neither input 1 or 2"),
}
}
In match statements, each comparison is done via the concept of arms. Each
“arm” is a case that rust will check for the correct value. However, covering
all cases is required, and if you want to do else case arm, then you use
either _ or you can do:
#![allow(unused)]
fn main() {
match "abc" {
a => println!("{}", a),
}
}
While Loops
Now let’s move onto the next statement, while loops. while loops allow for a
specific funtion to get repeated until that condition is no longer true.
#![allow(unused)]
fn main() {
let mut count = 0;
while count < 10 {
println!("The count is at: {}", count);
count += 1; // this statement is equivalent to count = count + 1
}
}
In the above code, we use a
count += 1which basically evaluates to setcountto the current value ofcount + 1. You can replace the+operator with-for minus,*for multiplication, or/for division.
Finally, we will cover for loops.
For loops are used to go through items inside an iterator. (Any data type which stores multiple elements, Vectors are one of the common ones)
How would you print all of the elements below?:
#![allow(unused)]
fn main() {
let animals = vec!["chicken", "snake", "dog", "cat"];
}
One way would be to manually print each element, but thats not scalable, and is quite frankly, annoying. This is where for loops are used.
#![allow(unused)]
fn main() {
let animals = vec!["chicken", "snake", "dog", "cat"];
for animal in animals {
println!("This animal is: {animal}");
}
}
It allows you to dynamically do something with all the elements.
You can also put in a Range of values.
#![allow(unused)]
fn main() {
let target = 10;
for i in 0..target {
println!("The current number is: {}", i);
}
}
But be careful, only use for loops for iterablesa and while loops only while needed