This note includes a quick set of notes I took while learning [[Rust]] using [The Rust Programming Language](https://doc.rust-lang.org/book/), a free book to get started on the language.
---
## Chapter 1 — Getting Started
### Installation
Installing Rust is really simple:
```bash
curl --proto '=https' --tlsv1.2 -sSf <https://sh.rustup.rs> | sh
```
After running this command and making sure `$HOME/.cargo/bin` is in the `$PATH`, all the utilities should be ready to use. These utilities are able to update themselves, using:
```bash
# Update the toolkit
$ rustup update
# Uninstall
$ rustup self uninstall
```
It is also possible to check the Rust documentation offline by running `rustup doc`.
### Hello, World!
Now that everything is ready, we can write our first "Hello, world!" program. The code for this example and the rest of the book is available in the local project `learn-rust`.
The program itself is as straightforward as it can be:
```rust
fn main() {
println!("Hello, world!");
}
```
And the compilation is as well pretty straightforward:
```bash
$ rustc main.rs
$ ./main
Hello, world!
```
### Hello, Cargo!
The next section expands the previous program to be developed as a Cargo crate. Cargo is Rust's build system and package manager. To create a project with Cargo:
```bash
$ cargo new hello_cargo
```
What has Cargo exactly done?
1. It has created a git repository and an adequate gitignore.
2. It has added a `Cargo.toml` configuration file with sensible defaults.
3. It has created an example `src/main.rs`
To build and run a Cargo project:
```bash
# To create and run a debug version (with no code optimizations)
$ cargo build
Compiling hello-cargo v0.1.0 (/home/dvicente/dev/learn-rust/hello-cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.19
$ ./target/debug/hello_cargo
Hello, world!
# To create and run a release version
$ cargo build --release
Compiling hello-cargo v0.1.0 (/home/dvicente/dev/learn-rust/hello-cargo)
Finished release [optimized] target(s) in 0.17s
$ ./target/release/hello_cargo
Hello, world!
```
When building for the first time, Cargo creates a `Cargo.lock` file that locks the dependency versions.
---
## Chapter 2 — Programming a Guessing Game
The chapter is pretty straightforward and the code can be easily followed even if you don't know Rust. The code is also in the repository, inside the crate `guessing_game`.
---
## Chapter 3 — Common Programming Concepts
### Variables
As a language, one of Rust's main objectives is to provide safety, and there are several ways it tries to achieve it. In Rust, **variables are immutable by default**. This forces you to use `mut` keyword to explicitly state when a variable can be mutated. The difference with constants is that constants (declared with the word `const` instead of `let`) are that the latter must be declared with an explicit data type and can be declared in any scope.
Even if they are immutable, variables can be shadowed by using the `let` keyword again, which allows to even change the data type of the variable.
### Datatypes
Rust is strongly and statically typed, so all data types must be known at compile time. Regular datatypes are what you could expect (numeric, Unicode-based characters, etc). This notes will focus mainly in the compound types.
The **tuple type** is a way of grouping together a fixed number of values with a variety of types into one type. It can also be deconstructed (similar to Python's unpacking).
```rust
let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup;
```
An **array** is a way of grouping together a _fixed_ number of values of a single data type. It is allocated on the stack. The length of the array can be declared differently in the code, as per the following snippet.
```rust
let xs: [i32; 5] = [1, 2, 3, 4, 5];
```
This syntax is similar to the repetition one: in order to create an array of length 5 where all the elements are 3, you could use:
```rust
let xs = [3; 5];
```
Elements of an array can be accessed using the squared brackets notation and the first element is `0`. An out-of-bounds access will result in a _runtime_ error.
### Functions
This is pretty straightforward and an example should be enough:
```rust
fn plus_one(x: i32) -> i32 {
x + 1
}
```
### Control flow
There are several ways to control the execution of a Rust program:
```rust
// if-else statement
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 3, or 2");
}
// let-if statement
let number = if condition { 5 } else { 6 };
// simplest loop -- can use continue or break
loop {
println!("forever!");
}
// Simple while loop
while number != 0 {
println!("{}!", number);
number -= 1;
}
// for loops are based on for-each
let a = [1, 2, 3, 4, 5]; // works like `(1..5)`
for element in a.iter() {
println!("the value is: {}", element);
}
```
---
## Chapter 4 — Understanding Ownership
To understand ownership, we first must clarify once again what the stack and the heap are. Both are parts of memory used to store code at runtime, but with structural differences:
- The **stack** is structured as _last in, first out_ (LIFO) and only variables whose _size is fixed and known at compile_ time are stored in the stack.
- The **heap** is where the variables with _unknown size_ are stored. Space in the heap must be requested (allocating) and it is referred to with a pointer (its address).
By this nature, using the stack is always faster since no allocation is needed — there is always the necessary size at the top of the stack. Also, no pointers are needed — the data needed is always on top of the stack. **Ownership is a mechanisms to organize the data on the heap**.
### Ownership rules
There are three rules to take into account regarding ownership:
1. Each value in Rust has a variable that's called its **owner**.
2. There can only be **one owner at a time**.
3. When the **owner goes out of scope**, the value is **destroyed**.
### References and Borrowing
References (`&`) are the mechanism used to manipulate values when the scope changes and we do not wish them to be dropped.
```rust
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
```
This mechanism to passing references without changing ownership is, adequately, called **borrowing**. In the example above, `s` cannot be mutated in the function — it does not own it.
To fix this is possible to use **mutable references**, which allow the function to modify the values which the original variable points to.
```rust
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
```
There is one important restriction: you can only have **one mutable reference to a particular piece of data in a particular scope**. A corollary to this is that no regular references can be created in a scope when a mutable reference exists. This mechanism is created to prevent data races. Note that a reference scope starts from where it is introduced until its last time it is used. Therefore, this is valid code:
```rust
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2);
// r1 and r2 are no longer used after this point
let r3 = &mut s; // no problem
println!("{}", r3);
```
The chapter ends with a note about dangling references and how to prevent them in Rust (just don't return dangling references, basically).
### Slices
In Rust, slices are references to a part of a collection. The notation is similar to that of range's, as can be seen in the example below:
```rust
let s = String::from("hello world");
// the type of a string slice is `&str`
let hello = &s[0..5];
let world = &s[6..11];
```
The type of a string slice is `&str` because string literals are actually slices! They point to a certain value inside the binary itself.
When using string slices as parameters, it is usually more flexible to always use `&str` instead of `&String`:
```rust
// assuming a function `fn first_word(s: &str) -> &str`
fn main() {
let my_string = String::from("hello world");
// first_word works on slices of `String`s
let word = first_word(&my_string[..]);
let my_string_literal = "hello world";
// first_word works on slices of string literals
let word = first_word(&my_string_literal[..]);
// Because string literals *are* string slices already,
// this works too, without the slice syntax!
let word = first_word(my_string_literal);
}
```
Slices can also be applied to other collections! For example, this code is valid, and `slice` is of type `&[i32]`:
```rust
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
```
---
## Chapter 5 — Using Structs to Structure Related Data
A struct is a custom data type that lets you name and package together multiple related values that make up a meaningful group, for example:
```rust
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
```
Initializing a struct is pretty straightforward:
```rust
let user1 = User {
email: String::from("
[email protected]"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
```
A struct must be declared as mutable in order to change any of their fields. The whole instance is mutable in that case.
It is convenient to use the _field init shorthand_ when the name of a field is the same as a variable:
```rust
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
```
There is another shorthand calle _struct update syntax_, that allows to update certain values of another instance and reuse the rest when creating a new one:
```rust
let user2 = User {
email: String::from("
[email protected]"),
username: String::from("anotherusername567"),
..user1
};
```
There is a simple version of the structs called _tuple structs_, which do not have named fields:
```rust
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
```
### Method syntax
You can define methods for the structs. The definition of the methods are, interestingly enough, not coupled with the actual struct's definition: instead, they are added in `impl` blocks:
```rust
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
```
The first one is a method, which can be called on an instance using `rect.area()`. The latter is an associated function and does not take an instance as a parameter, it can be called using `Rectangle::square(3)`.
---
## Chapter 6 — Enums & Pattern Matching
In Rust, enums are closer to algebraic data types than regular C enums. The definition is straightforward enough:
```rust
// Simpler way to define an enum
enum IpAddrKind {
V4,
V6,
}
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
// Enum with data in the variants
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
```
To unpack the values inside the enum types, probably the most useful feature is pattern matching using `match`:
```rust
fn connect_to(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(addr) => connect_ip4(addr),
IpAddr::V6(addr) => {
println!("Using IPv6...");
connect_ip6(addr)
}
}
}
```
`match` is exhaustive, so all the possibilities must be covered. To achieve it, the special pattern `_` can be used as a wildcard and last arm.
The last control mechanism to take into account is the `if let` construct, which allows a simple and concise way to perform a single deconstruction.
```rust
let ip = IpAddr::V6("::1");
if let IpAddr::V4(addr) = ip {
println!("{} is an IPv4 address");
} else {
println!("Not an IPv4");
}
```
---
## Chapter 7 — Managing Growing Projects with Packages, Crates and Modules
Rust has a number of features that allow you to manage your code’s organization, including which details are exposed, which details are private, and what names are in each scope in your programs. These features, sometimes collectively referred to as the _module system_, include:
- **Packages:** A Cargo feature that lets you build, test, and share crates
- **Crates:** A tree of modules that produces a library or executable
- **Modules** and **use:** Let you control the organization, scope, and privacy of paths
- **Paths:** A way of naming an item, such as a struct, function, or module
### Packages and Crates
A **crate** is a binary or library; whose root is a source file hat the Rust compiler starts from; it also makes up the root module of a crate. A **package** is one or more crates that provide a set of functionality; it is defined in its `Cargo.toml` how to build those crates.
### Defining Modules to Control Scope and Privacy
**Modules** let us organize code within a crate in froups for readability and easy reuse. Modules also control the _privacy_ of items: whether an item can be used by outside code (public) or is an internal implementation detail and not available for outside use (private).
### Paths for Referring to an Item in the Module Tree
There are two different ways to refer to an item:
- An **absolute path** starts from a crate root by using a crate name or a literal `crate`.
- A relative path starts from the current module and uses `self`, `super` or an identifier in the current module.
```rust
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
// Different crate ---
fn serve_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
// Relative path using super
super::serve_order();
}
fn cook_order() {}
}
```
---
## Chapter 8 — Common Collections
### Vectors
The first collection type that we can take a look at is `Vec<T>`. Creating a new vector can be done two different ways:
```rust
// Creating an empty vector
let v1: Vec<i32> = Vec::new();
v1.push(1);
v1.push(2);
v1.push(3);
// Creating a vector with initial values
let v2: = vec![1, 2, 3];
```
Reading elements of vectors use familiar syntaxes for the most part:
```rust
let v = vec![1, 2, 3, 4, 5];
// Regular way to access elements returns a reference
let third: &i32 = &v[2];
println("The third element is {}", third);
// Using v.get returns an Option<&T>
match v.get(2) {
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element."),
}
```
Accessing a reference of an element that does not exist will cause the code to panic, and all references created before mutating the vector are invalidated after the mutable borrow.
Iterating over the vector can be done using a simple `for` loop:
```rust
let v = vec![1, 2, 3];
// immutable iteration
for i in &v{
println!("{}", i);
}
// mutable iteration
for i in &mut v{
*i += 50;
}
```
It is also an useful technique to use pre-defined enums to store multiple types in a vector:
```rust
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
```
If you don't know the exhaustive set of types to be defined at compile time, this won't work. In that case, maybe a trait object is a better choice.
### Strings
Rust has only one string type in the core language, which is the string slice `str` that is usually seen in its borrowed form (`&str`). String slices are references to some UTF-8 encoded string data stored elsewhere, for instance string literals, which are stored in the program's binary.
On the other hand, the `String` type is provided by Rust's standard library (rather than coded into the language) and is a growable, mutable, owned, UTF-8 encoded string type. When referring to "strings", we usually mean `String` and `&str` types.
The basic operations are straightforward:
```rust
// Empty string
let mut s = String::new();
// String from literal
let s = String::from("initial contents");
// String from a str variable
let data = "initial contents";
let s = data.to_string();
// Updating a String
let mut s = String::from("foo");
s.push_str("bar");
s.push('!');
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // Note that s1 has been moved
```
It is important to understand that the Rust compiler is able to coerce the types `&String` and `&str` (as it is happening in the last line). This case is a _deref coercion_, and the compiler is able to turn `&s2` into `&s2[..]`. However, there is a better way to concatenate very complex strings:
```rust
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{}-{}-{}", s1, s2, s3);
```
Since both `str` and `String` are UTF-8 encoded, Rust does not support string indexing to prevent undesired behavior. Rust actually needs you to be more specific regarding what you want to access:
- You can access `s.chars()` to iterate the characters of a string.
- You can access `s.bytes()` to iterate the bytes that form the string.