Foreword

Hello. I hope this book finds you well.

This book is currently being written.

As a non-daily rustacean - I am a daily TypeScript developer and UI designer -, coming back to Rust only from time to time is sometimes painful. I tend to forget how to code with Rust. Although it is a very nice language, it is also a complex and explicit language. There is a lot to keep in mind.

This little book is the book I want to always have with me when I code with Rust. Unfortunately, it will not cover everything about Rust but, for sure, enough to let you code any kind of high-level application. If you want to go unsafe and manage the memory yourself, this book will certainly not cover what you need. To be fair, at this level of understanding of the language, you probably do not need this book anyway. I will assume, though, that you have already coded before, at least with another C-like language. I hope this book will help you as much as I had fun writing it.

Cédric Eberhardt.

Don't hesitate to contribute if you are missing something or if I did a mistake, which is to expect, as I still consider myself as a Rust beginner. Thank you in advance for the help.

If you want to play a bit with Rust without installing it, there is a Rust Playground for that.

Inspiration and references

Concise Rust

Short history of Rust

Rust began, in 2006, as a side project of Graydon Hoare, an employee at Mozilla. Mozilla believed in the potential of this new language and began sponsoring it. It was revealed to the world in 2010.

According to Hoare, the name of the language comes from the rust fungus. This has caused Rust programmers to adopt “Rustaceans” as their nickname of choice.

In mid-2020, following the layoffs at Mozilla, the Rust core team said they were considering a new Rust foundation to boost Rust independence. It was officially announced in February 2021.

Editions

  • Rust 2015: 1.0 (cargo’s edition has been introduced in 2017)
  • Rust 2018: 1.31 or edition="2018"
  • Rust 2021: 1.56 or edition="2021"

Why Rust?

Rust is a very nice language. It has been designed to cover most development areas, from an easy CLI application to system programming. And all of that with safety, control and productivity in mind.

Regarding safety, you can think of Python, and for control, C. Rust is some kind of mix of the best of both of these worlds. While I do not think Rust is a good replacement for Python as such, it is for sure a perfect replacement for C or any low-level languages, with the safety and fun of Python, but without trading the performance of C-like languages at the same time.

A more personal point of view, what I like too, is the fact that Rust is an explicit language. There is no magic. What you read if what it is going to happen. There are no tricks, like for instance in JavaScript, where Array and Object are passed by reference. If you don't know that, you will have weird side effects. In Rust, you always know what happens, even with mutability. You will definitely code with confidence.

The Rust programming language is fundamentally about empowerment: no matter what kind of code you are writing, Rust empowers you to reach further, to program with confidence in a wide variety of domains than you did before.

Foreword from the Rust programming language book.

In the other hand, it makes things a bit more complex and noisy. Well, this is the reason of this book and why Rust is known to have a steep learning curve.

What makes it different to most common programming languages?

  • It does not have a garbage collector. But you can still choose how the memory is managed. And you do not have to free the memory yourself. It has a lot of smart pointers for memory efficiency.
  • Because of how memory is managed - the ownership principle - it is memory and thread safe. There are no dangling pointers, no data race, no buffer overflow, no iterator invalidation.
  • It keeps performance, very close to what C can offer. Even sometimes faster.
  • No null pointer exceptions.
  • No class and so, no inheritance.
  • First-class WebAssembly support.

Ecosystem

Last but not least, although not related to the Rust language itself, the whole ecosystem and tooling around Rust is just awesome. It is definitely feature-complete:

  • A complete ecosystem (rustup, cargo, crates, rustfmt, clippy, ...)
  • Good IDE support (IntelliJ-based and VSCode)
  • Documentation directly in source code, from the comments
  • Examples folder managed with cargo
  • Tests in source code and cargo test
  • cargo can be expanded: cargo-edit, cargo-watch, ...
  • Compiler giving hints thanks to clippy

Variables and Data types

Variable binding and Type inference


#![allow(unused)]
fn main() {
let foo = bar;
}

This line creates a new variable named foo and binds it to the value of the bar variable.

You don't need to define the type on assignment. But you must bind a value before usage. The compiler can usually infer what type we want to use based on the value and how we use it.


#![allow(unused)]
fn main() {
let str; // declare str
str = "Hello, world!"; // assignment -> &str

let a = 40_000; // -> i32
let b = 40.; // -> f64
let v: [_; 5] = [3; 5];
//      ^ infers type for usage

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

The binding can be done at a later stage.


#![allow(unused)]
fn main() {
let a;
// ... some code
a = 42;
}

Of course, you can still specify the type manually.


#![allow(unused)]
fn main() {
let x: i32 = 45;
}

Unused variables

While you are coding, you can say the compiler to not complain about a variable not being used.


#![allow(unused)]
fn main() {
let _unused_variable = 42;
}

Data types

The types covered here are all stored on the stack. We will talk about the stack and the heap on another chapter. What you need to know for now is that the stack is the fast memory.

Scalar Types

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


#![allow(unused)]
fn main() {
let num: isize = 98_222;
let byt = b'A' // u8 only
let x = 2.0 // f64

let t: bool = true
}

When you are compiling in release mode, Rust does not check for integers overflow. If you want to wrap explicitly, you can use the standard library type Wrapping.


#![allow(unused)]
fn main() {
let zero = 0u8;

println!("{}", zero - 1_u8);
//             ^^^^^^^^^^^ attempt to compute `0_u8 - 1_u8`, which would overflow
}

#![allow(unused)]
fn main() {
use std::num::Wrapping;
let zero = Wrapping(0u8);
let one = Wrapping(1u8);

println!("{}", (zero - one).0); // -> 255
}

Rust’s char type is four bytes in size and represents a Unicode Scalar Value, which means it can represent a lot more than just ASCII. Accented letters; Chinese, Japanese and Korean characters; emoji; and zero-width spaces are all valid char values in Rust.


#![allow(unused)]
fn main() {
let heart_eyed_cat = '😻';
}

Compound Types

Rust has two primitive compound types: tuples and arrays.

The Tuple Type

This is a fixed length collections of values of different types. We can access a tuple element directly by using a . followed by the index of the value we want to access. The first index in a tuple is 0.


#![allow(unused)]
fn main() {
let tupl = ('x', 32); // => (char, i32)
let x = tupl.0; // => 'x'
let thirty_two = tupl.1; // => 32
}

As we will see in the "pattern" chapter, you can use pattern matching to destructure a tuple value.


#![allow(unused)]
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
}

The Array Type

Arrays in Rust have a fixed length, like tuples. Arrays are useful when you want your data allocated on the stack rather than the heap. You can access elements of an array using indexing. As for tuple, the first index is 0. If the index is greater than or equal to the length, Rust will panic because of Rust's safety principle.


#![allow(unused)]
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];

let first = a[0];
}

An array is not as flexible as the vector type. A vector (Vec) is a similar collection type provided by the standard library that is allowed to grow or shrink in size.

Immutable by default

Mutability in Rust is explicit. When a variable le is immutable, once a value is bound to a name, you can’t change the value.


#![allow(unused)]
fn main() {
struct Number { value: i32 }

let foo = 32;
foo = 40; // Nope

let n = Number {
    value: 3
};
n.value = 5; // Nope
n = Number { value: 5 } // Nope
}

Let's make it mutable. The entire instance must be mutable; Rust does not allow us to mark only certain fields as mutable.


#![allow(unused)]
fn main() {
struct Number { value: i32 }

let mut foo = 32;
foo = 40; // Yep

let mut n = Number {
    value: 3
};
n.value = 5; // Yep
n = Number { value: 5 } // Yep
}

Constants

Constants are like immutable variables, but there are some differences:

  • Declared using the const keyword.
  • The type must be annotated. You can not infer it.
  • You are not allowed to use mut with constants.
  • Set only to a constant expression, not to the result of a function call or any other value that could only be computed at runtime.

#![allow(unused)]
fn main() {
const MAX_POINTS: u32 = 100_000;
}

Shadowing

Shadowing is different from marking a variable as mut, because we will 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.


#![allow(unused)]
fn main() {
let x = 32;
let x = 32 + 8; // still immutable!

println!("{}", x); // => 40. Only the last x will be available
}

Even with a different type.


#![allow(unused)]
fn main() {
let x = 32;
let x = "Hey";

println!("{x}"); // => "Hey"
}

Functions, Blocks and Scope

A pair of brackets declares a block, which has its own scope. It can evaluate to a value and can contain multiple statements.

fn main() {
	let x = "out";
	{
		// this is a different `x`
		let x = "in";
		println!("{x}"); // => in
	}
	println!("{x}"); // => out
}

Functions

Functions are pervasive in Rust code. The main function is the most important function in the language. It's the entry point of many programs.

The fn keyword allows you to declare new functions. Rust code uses snake case as the conventional style for functions and variables names. Every function returns by default a unit type, written (). It's an empty tuple. No need to specify it or return it, it is implicit.

fn main() {
	// empty
}

Rust does not care where you define your functions, only that they are defined somewhere.

Parameters

In function signatures, you must declare the type of each parameter.


#![allow(unused)]
fn main() {
fn another_function(x: i32, y: i32) {
	println!("x: {}", x);
	println!("y: {}", y);
}
}

Return value

We do declare their type after an arrow ->.


#![allow(unused)]
fn main() {
fn add(x: i32, y: i32) -> i32 {
	return x + y;
}
}

Statements and Expressions in Function Bodies

Rust is an expression-based language. Statements are instructions that perform some action and do not return a value. For instance, the let y = 6 statement does not return a value. Expressions evaluate to a resulting value. Calling a function is an expression. Calling a macro is an expression. The block that we use to create new scopes, {}, is an expression.

Expressions do not include ending semicolons. If you add a semicolon to the end of an expression, you turn it into a statement, which will then not return a value.


#![allow(unused)]
fn main() {
fn add_2(a: i32) -> i32 {
	a + 2
}

println!("{}", add_2(5)); // => 7
}

Ownership, Borrowing and Slices

Ownership is one of the unique feature of Rust, and it enables Rust to make memory safety guarantees without needing a garbage collector.

Don’t try to fight against the borrow checker.

The Stack and the Heap

The stack stores values in the order it gets them and removes the values in the opposite order. This is referred to as last in, first out. All data stored in the stack must have a known, fixed size. Data with an unknown size at compile time or a size that might change must be stored on the heap instead.

The heap is less organized. The operating system finds an empty spot in the heap that is big enough, marks it as being in use, and returns a pointer, which is the address of that location. This process is called allocating on the heap.

Pushing to the stack is faster than allocating on the heap because the operating system never has to search for a place to store new data. Comparatively, allocating space on the heap requires more work, because the operating system must first find a big enough space to hold the data and then perform bookkeeping to prepare for the next allocation.

Accessing data in the heap is slower than accessing data on the stack because you have to follow a pointer to get there. Contemporary processors are faster if they jump around less in memory.

Memory and Allocation

As an example, there are two types of strings in Rust: String and &str.

  • A String is stored as a vector of bytes (Vec<u8>), but guaranteed to always be a valid UTF-8 sequence. String is heap allocated, growable and not null terminated. With the String type, in order to support a mutable, growable piece of text, we need to allocate an amount of memory on the heap, unknown at compile time, to hold the content.
  • &str is a slice (&[u8]) that always points to a valid UTF-8 sequence, and can be used to view into a String, just like &[T] is a view into Vec<T>. In the case of a string literal, we know the content at compile time, so the text is hardcoded directly into the final executable. This is why string literals are fast and efficient.

#![allow(unused)]
fn main() {
let mut s = String::From("hello");

s.push_str(", world!"); // appends a literal to a String
}

Drop

When a variable goes out of scope, Rust calls a special function for us. This function is called drop. Rust call drop automatically at the closing curly bracket.

Move


#![allow(unused)]
fn main() {
let x = 5;
let y = x;
}

bind the value 5 to x; then make a copy of the value in x and bind it to y.

This is indeed what is happening, because integers are simple values with a known, fixed size and these two 5 values are pushed on the stack.


#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let s2 = s1;
}

A String is made of three parts: a pointer to the memory that holds the content of the string, a length, and a capacity.

s1 data

namevalue
ptr
len5
capacity5

content

indexvalue
0h
1e
2l
3l
4o

When we assign s1 to s2, the String data is copied, meaning we copy the pointer, the length, and the capacity that are on the stack. We do not copy the data on the heap that the pointer refers to.

Again, when a variable goes out of scope, Rust automatically calls the drop function and cleans up the heap memory for that variable. When s2 and s1 go out of scope, they will both try to free the same memory. This is known as a double free error and is one of the memory safety bugs.

Instead of trying to copy the allocated memory, Rust considers s1 to no longer be valid and, therefore, Rust doesn't need to free anything when s1 goes out of scope.

If you’ve heard the terms shallow copy and deep copy while working with other languages, the concept of copying the pointer, length, and capacity without copying the data probably sounds like making a shallow copy. But because Rust also invalidates the first variable, instead of calling it a shallow copy, it’s known as a move.

Rust will never automatically create “deep” copies of your data. Therefore, any automatic copying can be assumed to be inexpensive in terms of runtime performance.

Clone


#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);
}

When you see a call to clone, you know that some arbitrary code is being executed and that code may be expensive. It’s a visual indicator that something different is going on.

Stack-Only Data: Copy

Types such as integers that have a known size at compile time are stored entirely on the stack, so copies of the actual values are quick to make. In other words, there’s no difference between deep and shallow copying here, so calling clone would not do anything different from the usual shallow copying, and we can leave it out.

Rust has a special annotation called the Copy trait that we can place on types that are stored on the stack like integers are.

Here are some of the types that implement Copy:

  • All the integer types, such as u32.
  • The Boolean type, bool, with values true and false.
  • All the floating point types, such as f64.
  • The character type, char.
  • Tuples, if they only contain types that also implement Copy.

Ownership and Functions

Passing a variable to a function will move or copy, just as assignment does. The ownership of a variable follows the same pattern every time: assigning a value to another variable moves it.

References and Borrowing

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()
}

These ampersands are references, and they allow you to refer to some value without taking ownership of it.

The opposite of referencing by using & is deferencing, which is accomplished with the dereference operator *.

Just as variables are immutable by default, so are references. But mutable references have one big restriction: you can have only one mutable reference to a particular piece of data in a particular scope.


#![allow(unused)]
fn main() {
let r1 = &mut s;
//       ------ first mutable borrow occurs here
let r2 = &mut s;
//       ^^^^^^ second mutable borrow occurs here

println!("{}, {}", r1, r2);
//                 -- first borrow later used here
}

The benefit of having this restriction is that Rust can prevent data races at compile time. As always, we can use curly brackets to create a new scope, allowing for multiple mutable references, just not simultaneous ones.

A similar rule exists for combining mutable and immutable references. We also cannot have a mutable reference while we have an immutable one.


#![allow(unused)]
fn main() {
let r1 = &s;
//       -- immutable borrow occurs here
let r2 = &s;
let r3 = &mut s;
//       ^^^^^^ mutable borrow occurs here

println!("{}, {}, and {}", r1, r2, r3);
//                         -- immutable borrow later used here
}

Dangling references

In language with pointers, it's easy to erroneously create a dangling pointer, a pointer that references a location in memory that have been given to someone else or freed. In Rust, if you have a reference to some data, the compiler will ensure that the data will not go out of scope before the reference to the data does.

fn main() {
	let ref_to_nothing = dangle();
}

fn dangle() -> &String {
//             ^ expected named lifetime parameter

	let s = String::from("hello");

	&s
}

Because s is created inside dangle, when the code of dangle is finished, s will be deallocated. But we tried to return a reference to it. That means this reference would be pointing to an invalid String.

The Slice Type

Another data type that does not have ownership is the slice. Slices let you reference a continuous sequence of elements in a collection rather than the whole collection.

A string slice, for instance, is a reference to part of a String.


#![allow(unused)]
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
}

With Rust's .. range syntax,

  • if you want to start at the first index (zero), you can drop the trailing number: [..5]
  • if your slice includes the last byte of the String, you can drop the trailing number: [6..]
  • you can also drop both values to take a slice of the entire String: [..]

Recall from the borrowing rules that if we have an immutable reference to something, we cannot also take a mutable reference. In the example bellow, because clear needs to truncate the String, it tries to take a mutable reference, which fails.


#![allow(unused)]
fn main() {
let word = first_word(&s)
//                    -- immutable borrow occurs here
s.clear();
//^^^^^^^ mutable borrow occurs here
println!("the first word is: {}", word);
//                                ---- immutable borrow later used here
}

Other Slices

  • String literals are slices
  • Arrays are a general slice type, like &[i32]

Recap

The Rules of Ownership

  • Each value in Rust has a variable that's called its owner.
  • There can be only one owner at a time.
  • When the owner goes out of scope, the value will be dropped.

The next example will fail as name has been passed to Person.first_name, bound to p and is not owned by name anymore.

struct Person {
	first_name: String,
}

fn main() {
	let name = String::from("Cédric");
	//  ---- move occurs because `name` has type `String`,
	//       which does not implement the `Copy` trait

	let p = Person { first_name: name };
	//                            ---- value moved here

	println!("{}", name);
	//             ^^^^ value borrowed here after move
}

It can be borrowed, though. For that you need to use references and lifetime tags.

The Rules of References

  • At any given time, you can have either one mutable reference or any number of immutable references.
  • References must always be valid.

The & indicates that this argument is a reference, which gives you a way to let multiple parts of your code access one piece of data without needing to copy that data into memory multiple times. References are a complex feature, one of Rust’s major advantages is how safe and easy it is to use references. References passed to Person<'a> will now be alive as long as its usage, which is the implicit lifetime of main().

struct Person<'a> {
	first_name: &'a str,
}

fn main() {
	let name = String::from("Cédric");
	let _p = Person { first_name: &name };

	println!("{}", name);
}

Or simply copy or clone the value. Copy is a shallow copy, while Clone is a deep clone.

struct Person {
	name: String,
}

fn main() {
	let name = format!("Hello, {}!", "World");
	let _p = Person { name: name.clone() };

	println!("{}", name);
}

Structs, Enums and Implementation

There is no class and no inheritance. Rust has only struct, impl and trait.

Struct

A struct is like an object's data attribute.

It's possible for structs to store references to data owned by something else, but to do so require the use of lifetimes.


#![allow(unused)]
fn main() {
struct Rectangle {
	width: u32,
	height: u32,
}

let rect = Rectangle { width: 3, height: 2 };
rect.width; // => 3
}

Tuple Structs

Tuple structs are useful when you want to give the whole tuple a name and make the tuple be a different type from other tuples, and naming each field as in a regular struct would be verbose or redundant.


#![allow(unused)]
fn main() {
struct Color(i32, i32, i32);
let black = Color(0, 0, 0);
}

Unit-Like Structs

You can also define structs that don't have any fields! These are called unit-like structs because they behave similarly to (), the unit type. Unit-like structs can be useful in situations in which you need to implement a trait on some type but don't have any data that you want to store in the type itself.

Method Syntax: Implementation


#![allow(unused)]
fn main() {
struct Rectangle {
	width: u32,
	height: u32,
}

impl Rectangle {
	fn area(&self) -> u32 {
		self.width * self.height
	}
}

let rect = Rectangle { width: 30, height: 50 };
println!("The are is {} square pixels.", rect.area());
}

Methods are similar to functions. To define the function within the context of Rectangle, we start an impl block. Then we move the area function within the impl curly brackets.

We can use the method syntax to call the area method on our Rectangle instance.

In the signature for area we use &self instead of rectangle: &Rectangle because Rust knows the type of self is Rectangle due to this method's being inside the impl Rectangle context. Methods can take multiple parameters that we add to the signature after the self parameter, and those parameters work just like parameters in functions.

Each struct is allowed to have multiple impl blocks.

Associated functions

Another useful feature of impl blocks is that we are allowed to define functions within impl blocks that do not take self as a parameter. These are called associated functions because they are associated with the struct. They are called after the :: syntax.

They're still functions, not methods, because they don't have an instance of the struct to work with, like String::from. An associated function is implemented on a type, rather than on a particular instance of a Rectangle. Some language call this a static method.

Associated functions are often used for constructors that will return a new instance of the struct. A common usage in the standard library and in the community is to define a new function. You can use the field init shorthand syntax to initialize a struct with variables.


#![allow(unused)]
fn main() {
impl Rectangle {
	fn new(width: u32, height: u32) -> Self {
		Rectangle { width, height }
	}
}

let rect = Rectangle::new(30, 20);
}

Rest and Destructuring

The rest operator .. allows to fill the holes. It is called the struct update syntax.

The rest must be the last and not be followed by a comma.


#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Vec2 {
	x: f32,
	y: f32
}

// Rest
let v1 = Vec2 { x: 1.0, y: 3.0 };
let v2 = Vec2 { y: 2.0, ..v1 };

// Destructuring a tuple
let (a, b) = (3, 7);

// Destructuring with Rest
let Vec2 { x,..} = v2;

println!("{:?}, {:?}, {:?}", v2, b, x) // => Vec2 { x: 1.0, y: 2.0 }, 7, 1.0
}

Throw away a value

During destructuring, if you don't want to deal with all values you can omit some with an underscore. In use with the rest operator .. it's very easy to just export what you need.


#![allow(unused)]
fn main() {
let _ = get_stuff(); // throws away the returned value

// The value 3 and the rest will not be assigned to a variable
let (_, b, ..) = (3, 7, 14, 45);
let (width, _) = get_size();
}

Function parameters

Function parameters can also be pattern.

fn print_coordinates(&(x, y): &(i32, i32)) {
    println!("Current location: ({}, {})", x, y);
}

fn main() {
    let point = (3, 5);
    print_coordinates(&point);
}

Enums

Rust's enums are most similar to algebraic data types in functional languages. Note that the variants of the enum are namespaced (::) under its identifier.

We can put data directly into each enum variant. You can put any kind of data inside an enum variant: strings, numeric types, or struct, for example. You can even include another enum.


#![allow(unused)]
fn main() {
enum IpAddr {
	V4(u8, u8, u8, u8),
	V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
}

The Billion Dollars Mistake

The problem with null values is that if you try to use a null value as a not-null value, you will get an error of some kind. As such, Rust does not have nulls, but it does have an enum that can encode the concept of a value being present or absent.

It is replaced by the Option<T> enumeration. The variants of Option are Some and None. The None variant represents no value while Some can hold one piece of data of any type.


#![allow(unused)]
fn main() {
enum Option<T> {
	Some(T),
	None,
}
}

If we use None rather that Some, we need to tell Rust what type of Option<T> we have.


#![allow(unused)]
fn main() {
let val1: Option<i32> = None;
let val2: Option<_> = Some(32);

println!("{:?}, {:?}", val1, val2);
}

Because Option<T> and T (where T can be any type) are different types, the compiler won't let us use an Option<T> value as if it were definitely a valid value.


#![allow(unused)]
fn main() {
let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y;
//          ^ no implementation for `i8 + Option<i8>`
}

Everywhere that a value has a type that is not an Option<T>, you can safely assume that the value is not null.

Error management

Rust doesn't have exceptions. Instead, it has the type Result<T, E> for recoverable errors and the panic! macro that stops execution when the program encounters an unrecoverable error.

Result is, like Option, also an enumeration. For Result<T, E>, the variants are Ok<T> and Err<E>. The Ok variant indicates the operation was successful, and inside Ok is the successfully generated value. The Err variant means the operation failed, and Err contains information about how or why the operation failed.


#![allow(unused)]
fn main() {
use std::fs::File;

let f = File::open("hello.txt");
let f = match f {
	Ok(file) => file,
	Err(error) => {
		panic!("Problem opening the file: {:?}", error)
	}
}
}

Shortcuts for Panic on Error: unwrap and expect

Using match can be a bit verbose. The Result<T, E> type has many helper methods.

One of those method is called unwrap. If the Result value is the Ok variant, unwrap will return the value inside the Ok. If the Result is the Err variant, unwrap will call the panic! macro.


#![allow(unused)]
fn main() {
let f = File::open("hello.txt").unwrap();
}

Another method, expect, which is similar to unwrap, lets us also choose the panic! error message.


#![allow(unused)]
fn main() {
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}

It would be appropriate to call unwrap when you have some other logic that ensures the Result will have an Ok value, but the logic isn't something the compiler understands.

Propagating Errors

This pattern of propagating errors is so common in Rust that Rust provides the question mark operator ? to make this easier. Error values that have the ? operator called on them go through the from function, defined in the From trait in the standard library, which is used to convert errors from one type into another.

The ? operator eliminates a lot of boilerplate and makes this function's implementation simpler. We could even shorten the code further by chaining method calls immediately after the ?.


#![allow(unused)]
fn main() {
fn read_username_from_file() -> Result<String, io::error> {
	let mut s = String::new();

	File::open("hello.txt")?.read_to_string(&mut s)?;

	Ok(s);
}
}

The ? operator can only be used in functions that have a return type of Result.

use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
	let f = File::open("hello.txt")?;

	Ok(());
}

The Box<dyn Error> type is called a trait object. For now, you can read Box<dyn Error> to mean "any kind of error".

Generic Types, Traits and Lifetimes

Generics

Generics are abstract stand-ins for concrete types or other properties.

We can use generics to create definitions for items like function signatures or structs, which we can then use with many concrete data types.

To define the generic largest function, place type name declaration inside angle brackets, <>, between the name of the function and the parameter list.


#![allow(unused)]
fn main() {
fn largest<T>(list: &[T]) -> T {}
}

We can also define structs to use a generic type parameter in one or more fields using the <> syntax.


#![allow(unused)]
fn main() {
struct Point<T> {
	x: T,
	y: T,
}
}

To define a Point struct where x and y are both generics but could have different types, we can use multiple generic type parameters.


#![allow(unused)]
fn main() {
struct Point<T, U> {
	x: T,
	y: U,
}
}

You can use as many generic type parameters in a definition as you want, but using more than a few makes your code hard to read.

We can define enums to hold generic data types in their variants as we just see in Option<T> that returns Some<T> for instance.

We can also implement methods on structs and enums and use generic types in their definitions, too.


#![allow(unused)]
fn main() {
struct Point<T> {
	x: T,
	y: T,
}

impl<T> Point<T> {
	fn x(&self) -> &T {
		&self.x
	}
}
}

Note that we have to declare T just after impl so we can use it to specify that we are implementing methods on the type Point<T>. By declaring T as a generic type after impl, Rust can identify that the type in the angle brackets in Point is a generic type rather than a concrete type.

We could, for example, implement methods only on Point<f32> instances rather than on Point<T> instances with a generic type.


#![allow(unused)]
fn main() {
impl Point<f32> {
	fn distance_form_origin(&self) -> f32 {
		(self.x.powi(2) + self.y.powi(2)).sqrt()
	}
}
}

Generic type parameters in a struct definition aren't always the same as those you use in that struct`s method signatures.


#![allow(unused)]
fn main() {
struct Point<T, U> {
	x: T,
	y: U,
}

impl<T, U> Point<T, U> {
	fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
		Point {
			x: self.x
			y: other.y
		}
	}
}
}

Rust implements generics by performing monomorphization of the code that is using generics at compile time. Monomorphization is the process of turning generic code into specific code by filling in the concrete types that are used when compiled. With that, there is no runtime cost.

Traits

Trait definitions are a way to group method signatures together to define a set of behaviors necessary to accomplish some purpose. They are some kind of interfaces.

trait Geometry {
	fn area(&self) -> u32;
}

struct Rectangle {
	width: u32,
	height: u32,
}

impl Rectangle {
	// Common Rust way to do a "constructor"
	fn new(width: u32, height: u32) -> Self {
		Self { width, height }
	}
}

impl Geometry for Rectangle {
	fn area(&self) -> u32 {
		self.width * self.height
	}
}

fn main() {
	let rect = Rectangle::new(20, 30);

	println!(
		"The area of the rectangle is {} square pixels.",
		rect.area()
	);
}

The difference is that after impl, we put the trait name that we want to implement, then use the for keyword, and then specify the name of the type we want to implement the trait for. Within the impl block, we put the method signatures that the trait definition has defined.

One restriction to note with trait implementations is that we can implement a trait on a type only if at least one of the trait or the type is local to our crate. But we can’t implement external traits on external types. This restriction is part of a property of programs called coherence, and more specifically the orphan rule, so named because the parent type is not present. This rule ensures that other people’s code can’t break your code and vice versa. Without the rule, two crates could implement the same trait for the same type, and Rust would not know which implementation to use.

Default Implementations

Sometimes it’s useful to have default behavior for some or all of the methods in a trait instead of requiring implementations for all methods on every type.


#![allow(unused)]
fn main() {
pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}
}

To use a default implementation we specify an empty impl block. Default implementations can call other methods in the same trait, even if those other methods don’t have a default implementation.

Traits as Parameters

We can define a function that calls the method on its parameter, which is of some type that implements the trait. To do this we can use the impl Trait syntax.


#![allow(unused)]
fn main() {
pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}
}

The impl Trait syntax works for straightforward cases but is actually syntax sugar for a longer form, which is called a trait bound.


#![allow(unused)]
fn main() {
pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}
}

We can also specify more than one trait bound.


#![allow(unused)]
fn main() {
pub fn notify(item: &(impl Summary + Display)) {}
pub fn notify<T: Summary + Display>(item: &T) {}
}

Rust has alternate syntax for specifying trait bounds inside a where clause after the function signature.


#![allow(unused)]
fn main() {
fn some_function<T, U>(t: &T, u: &U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{}
}

Returning Types that Implement Traits

We can also use the 'impl Trait' syntax in the return position to return a value of some type that implements a trait.


#![allow(unused)]
fn main() {
fn returns_summarizable() -> impl Summary {
	Tweet {
		// --snip--
	}
}
}

The ability to return a type that is only specified by the trait it implements is especially useful in the context of closures and iterators. However, you can only use the impl Trait if you are returning a single type.


#![allow(unused)]
fn main() {
fn returns_summarizable(switch: bool) -> impl Summary {
	if switch {
		NewsArticle {
			// --snip--
		}
	} else {
		Tweet {
			// --snip--
		}
	}
}
}

Returning either a NewsArticle or a Tweet isn't allowed due to restrictions around how the impl Trait is implemented in the compiler.

Derivable Traits

TODO: Do the macro chapter first

Using Trait Bounds to Conditionally Implement Methods

By using a trait bound with an impl block that uses generic type parameters, we can implement methods conditionally for types that implement the specified traits.


#![allow(unused)]
fn main() {
struct Pair<T> {
	x: T,
	y: T,
}

impl<T: Display + PartialOrd> Pair<T> {
	// --snip--
}
}

We can also conditionally implement a trait for any type that implements another trait. Implementations of a trait on any type that satisfies the trait bounds are called blanket implementations and are extensively used in the Rust standard library.


#![allow(unused)]
fn main() {
impl<T: Display> ToString for T {
    // --snip--
}
}

In dynamically typed languages, we would get an error at runtime if we called a method on a type which did not define the method. But Rust moves these errors to compile time, so we’re forced to fix the problems before our code is even able to run. Additionally, we don’t have to write code that checks for behavior at runtime because we’ve already checked at compile time.

Lifetimes

Most of the time, lifetimes are implicit and inferred, just like most of the time, types are inferred. We must annotate types when multiple types are possible. In a similar way, we must annotate lifetimes when the lifetimes of references could be related in a few different ways

The main aim of lifetimes is to prevent dangling references, which cause a program to reference data other than the data it is intended to reference.

The Borrow Checker

The Rust compiler has a borrow checker that compares scopes to determine whether all borrows are valid.

fn main() {
	let r;                // ---------+-- 'a
						  //          |
	{                     //          |
		let x = 5;        // -+-- 'b  |
		r = &x;           //  |       |
	}                     // -+       |
						  //          |
	println!("r: {}", r); //          |
}                         // ---------+

#![allow(unused)]
fn main() {
fn longest(x: &str, y: &str) -> &str {
//            ----     ----     ^ expected named lifetime parameter
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

//   = help: this function's return type contains a borrowed value,
//           but the signature does not say whether it is borrowed from `x` or `y`
// help: consider introducing a named lifetime parameter
//   |
//   | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
//   |           ++++     ++          ++          ++
}

Lifetime annotation

Lifetime annotations have a slightly unusual syntax: the names of lifetime parameters must start with an apostrophe (') and are usually all lowercase and very short, like generic types. Most people use the name 'a. We place lifetime parameter annotations after the & of a reference, using a space to separate the annotation from the reference’s type.


#![allow(unused)]
fn main() {
&i32        // a reference
&'a i32     // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
}

As with generic type parameters, we need to declare generic lifetime parameters inside angle brackets between the function name and the parameter list. The constraint we want to express in this signature is that the lifetimes of both of the parameters and the lifetime of the returned reference are related such that the returned reference will be valid as long as both the parameters are.


#![allow(unused)]
fn main() {
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
	// --snip--
}
}

Remember, when we specify the lifetime parameters in this function signature, we’re not changing the lifetimes of any values passed in or returned. Rather, we’re specifying that the borrow checker should reject any values that don’t adhere to these constraints. In other words, the generic lifetime 'a will get the concrete lifetime that is equal to the smallest of the lifetimes of x and y.

The way in which you need to specify lifetime parameters depends on what your function is doing.

So far, we’ve only defined structs to hold owned types. It’s possible for structs to hold references, but in that case we would need to add a lifetime annotation on every reference in the struct’s definition. We declare the name of the generic lifetime parameter inside angle brackets after the name of the struct, so we can use the lifetime parameter in the body of the struct definition.


#![allow(unused)]
fn main() {
struct ImportantExcerpt<'a> {
    part: &'a str,
}
}

Lifetime elision

You have learned that every reference has a lifetime and that you need to specify lifetime parameters for functions or structs that use references. The Rust team found that Rust programmers were entering the same lifetime annotations over and over in particular situations. These situations were predictable and followed a few deterministic patterns. The developers programmed these patterns into the compiler’s code so the borrow checker could infer the lifetimes in these situations and would not need explicit annotations.

The patterns programmed into Rust’s analysis of references are called the lifetime elision rules. Lifetimes on function or method parameters are called input lifetimes, and lifetimes on return values are called output lifetimes.


#![allow(unused)]
fn main() {
impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}
}

The lifetime parameter declaration after impl and its use after the type name are required, but we’re not required to annotate the lifetime of the reference to self because of the first elision rule.

Check the Rustonomicon

The Static Lifetime

One special lifetime we need to discuss is 'static, which means that this reference can live for the entire duration of the program. All string literals have the 'static lifetime.


#![allow(unused)]
fn main() {
let s: &'static str = "I have a static lifetime.";
}

Collections

Unlike the built-in array and tuple types, the data these collections point to is stored on the heap, which means the amount of data does not need to be known at compile time.

Vectors

Vectors Vec<T> allow you to store more than on value in a single data structure that puts all the values next to each other in memory. Vectors can only store values of the same type T.


#![allow(unused)]
fn main() {
let v: Vec<i32> = Vec::new();
v.push(1);
// ...
// or
let v = vec![1, 2, 3];
}

Rust provides the vec!macro for convenience.

Reading Elements of Vectors

There are two ways to reference a value stored in a vector.


#![allow(unused)]
fn main() {
let v = vec![1, 2, 3, 4, 5];

let third: &i32 = &v[2];

match v.get(2) {
    Some(third) => println!("The third element is: {}.", third);
    None => println!("There is no third element.");
}
}

The first [] method may cause the program to panic if it references a nonexistent element, like &v[100]. When the get method is passed an index that is outside the vector, it returns None without panicking.

When the program has a valid reference, the borrow checker enforces the ownership and borrowing rules to ensure this reference and any other references to the contents of the vector remain valid.


#![allow(unused)]
fn main() {
let first = &v[0];
//           - immutable borrow occurs here
v.push(6);
//^^^^^^^ mutable borrow occurs here
println!("The first element is: {}", first);
//                                   ----- immutable borrow later used here
}

This error is due to the way vectors work: adding a new element onto the end of the vector might require allocating new memory and copying the old elements to the new space. In that case, the reference to the first element would be pointing to deallocated memory.

Iterating over the Values in a Vector

If we want to access each element in a vector in turn, we can iterate through all the elements rather than use indices to access one at a time.

We can also iterate over mutable references to each element in a mutable vector in order to make changes to all the elements.


#![allow(unused)]
fn main() {
let mut v = vec![100, 32, 57];
for i in &mut v {
    *i += 50;
}
}

To change the value that the mutable reference refers to, we have to use the dereference operator * to get to the value in i before we can use the += operator.

Using an Enum to Store Multiple Types

We said that vectors can only store values that are the same type. Fortunately, the variant of an enum are defined under the same enum type, so when we need to store elements of a different type in vector, we can define and use an enum.


#![allow(unused)]
fn main() {
enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("blue")),
    SpreadsheetCell:: Float(10.12),
];
}

Storing UTF-8 Encoded Text with 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.

The String type, which is provided by Rust`s standard library rather than coded into the core language, is a growable, mutable, owned, UTF-8 encoded string type.


#![allow(unused)]
fn main() {
let mut s = String::new();
s.push_str("hello");
let h = s[0];
}

#![allow(unused)]
fn main() {
let s1 = String::from("tic");
let s1 = String::from("tac");
let s1 = String::from("toe");

let s = s1 + "-" + &s2 + "-" + &s3; // note s1 has been moved here
let s = format!("{}-{}-{}", s1, s2, s3);
}

A String is a wrapper over a Vec<u8>.

In UTF-8, each Unicode scalar value may take more than one byte of storage. Think of Cyrillic, Japanese or Hindi signs. Therefore, an index into the string's bytes will not always correlate to a valid Unicode scalar value.

Another point about UTF-8 is that there are actually three relevant ways to look at strings from Rust's perspective: as bytes, scalar values, and grapheme clusters (the closest thing to what we would call letters).

Sometimes when you extract chars from a string, you will have diacritics that do not make sense on their own. So, indexing into a string is often a bad idea because it is not clear what the return type of the string-indexing operation should be: a byte value, a character, a grapheme cluster, or a string slice.

Getting grapheme clusters from strings is complex, so this functionality is not provided by the standard library. You can check the unicode-segmentation crate.

Rust has chosen to make the correct handling of String data the default behavior for all Rust programs, which means programmers have to put more thought into handling UTF-8 data up front. This trade-off exposes more of the complexity of strings than is apparent in other programming languages, but it prevents you from having to handle errors involving non-ASCII characters later in your development life cycle

Hash Maps

The type HashMap<K, V> stores a mapping of keys of type K to values of type V.

Each key can only have one value associated with it at a time.


#![allow(unused)]
fn main() {
use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
}

Of our three common collections, this one is the least often used, so it is not included in the features brought into scope automatically in the prelude.

Another way of constructing a hash map is by using the collect method on a vector of tuples, where each tuple consists of a key and its value.


#![allow(unused)]
fn main() {
let map = vec![(String::from("Blue"), 10), (String::from("Yellow"), 50)].collect();
}

We could also use the zip method to create a vector of tuples.


#![allow(unused)]
fn main() {
use std::collections::HashMap;

let teams = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];

let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
}

For types that implements the Copytrait, such as i32, the values are copied into the hash map. For owned values such as String, the values will be moved and the hash map will be the owner of those values.

Accessing Values

We can get a value out of the hash map by proving its key to the get method. The result is wrapped in Some because get returns an Option<&V>.

We can iterate over each key/value pair in a similar manner as we do with vectors, using a for loop.


#![allow(unused)]
fn main() {
for (key, value) in &scores {
    println!("{}: {}", key, value);
}
}

Updating a Hash Map

If we insert a key and a value into a hash map and then insert that same key with a different value, the value associated with that key will be replaced.

Hash Maps have a special API for inserting a value only if the key has no value, called entry. It takes the key you want to check as a parameter. The return value of the entry method is an enum called Entry that represents a value that might or might not exist.


#![allow(unused)]
fn main() {
use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);

scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
}

The or_insert method on Entry is defined to return a mutable reference to the value for the corresponding Entry key if that exists, and if not, inserts the parameter as the new value for this key and returns a mutable reference (&mut V) to the new value.

By default, HashMap uses a cryptographically strong hashing function that is not the fastest algorithm available. You can switch to another function by specifying a different hasher. A hasher is a type that implements the BuildHashertrait.

Closures and Iterators

Closures

Rust's closures are anonymous functions you can save in a variable or pass as arguments to other functions.


#![allow(unused)]
fn main() {
let x = vec![1, 2, 3, 4]
    .iter()
    .map(|x| x + 3);
}

To define a closure, we start with a pair of vertical pipes (|), inside which we specify the parameters to the closure. After the parameters, we place curly brackets that hold the body of the closure – these are optional if the closure body is a single expression.

Closures don't require you to annotate the types of the parameters or the return value like fn functions do. But as with variables, we can add type annotations of we want to increase explicitness and clarity at the cost of being more verbose than is strictly necessary.


#![allow(unused)]
fn main() {
let closure_annotated = |i: i32| -> i32 { i + 1 };
let closure_inferred = |i | i + 1 ;
}

We can define a closure and store the closure in a variable rather than storing the result of the function call.

The first time we call a closure with any value, the compiler infers the type of it and the return type of the closure. Those types are the locked into the closure, and we get a type error if we try to use a different type with the same closure.

Capturing the Environment with Closures

Closures have an additional capability that functions don`t have: they can capture their environment and access variables from the scope in which they are defined.


#![allow(unused)]
fn main() {
let x = 4;
let equal_to_x = |z| z == x;
let y = 4;
assert!(equal_to_x(y));
}

Closures can capture values from their environment in three ways, which directly map to the three ways a function can take a parameter: taking ownership, borrowing mutably, and borrowing immutably. These are encoded in the three Fn traits.

The Fn traits are provided by the standard library. All closures implement at least one of the traits: Fn, FnMut or FnOnce.

TraitDescription
FnOnceconsumes the variables it captures from its enclosing scope, known as closure's environment. To consume the captured variables, the closure must take ownership of these variables and move them into the closure when it is defined. The Once part represents the fact that the closure can not take ownership more than once, so it can be called only once.
FnMutcan change the environment be it mutably borrows values
Fn borrows value from the environment immutability

If you want to force the closure to take ownership of the values it uses in the environment, you can use the move keyword before the parameter list.


#![allow(unused)]
fn main() {
let equal_to_x = move |z| z == x;
}

Iterators

The iterator pattern allows you to perform some task on a sequence of items in turn. An iterator is responsible for the logic of iterating over each item and determining when the sequence has finished.

In Rust, iterators are lazy, meaning they have no effect until you call methods that consume the iterator to use it up.


#![allow(unused)]
fn main() {
let v = vec![1, 2, 3];
let v_iter = v.iter();

for val in v_iter {
	// -- snip --
}
}

All iterators implement a trait named Iterator that is defined in the standard library.


#![allow(unused)]
fn main() {
pub trait Iterator {
	type Item;

	fn next(&mut self) -> Option<Self::Item>;

	// -- snip --
}
}

Notice this definition uses some new syntax: type Item and Self::Item, which are defining an associated type with this trait. The Item type will be the type returned from the iterator.


#![allow(unused)]
fn main() {
let v = vec![1, 2, 3];
let mut v_iter = v.iter();

assert_eq!(v_iter.next(), Some(&1));
assert_eq!(v_iter.next(), Some(&2));
assert_eq!(v_iter.next(), Some(&3));
assert_eq!(v_iter.next(), None);
}

We need to make iterator mutable: calling the next method on an iterator changes internal state that the iterator uses to keep track of where it is in the sequence.

We did not need to make v_iter mutable when we used a for loop because the loop took ownership of v_iter and made it mutable behind the scenes.

The iter method produces an iterator over immutable references. If we want to create an iterator that takes ownership of v and returns owned values, we can call into_iter instead of iter. Similarly, if we want to iterate over mutable references, we can call iter_mut instead of iter.

Creating our own Iterators

The Iterator trait has a number of different methods with default implementations provided by the standard library. Other methods defined on the Iteratortrait, known as iterator adaptors, allow you to change iterators into different kinds of iterators.


#![allow(unused)]
fn main() {
struct Counter {
	count: u32,
}

impl Counter {
	fn new() -> Self {
		Counter { count: 0 }
	}
}

impl Iterator for Counter {
	type Item = u32;

	fn next(&mut self) -> Option<Self::Item> {
		self.count += 1;

		if self.count < 6 {
			Some{self.count}
		} else {
			None
		}
	}
}
}

We implemented the Iterator trait by defining the next method, so we can now use any Iterator trait method's default implementations as defined in the standard library, because they all use the next method's functionality.


#![allow(unused)]
fn main() {
let sum: u32 = Counter::new().zip(Counter::new().skip(1))
							 .map(|(a, b)| a * b)
							 .filter(|x| x % 3 == 0)
							 .sum();
asset_eq!(sum, 18);
}

Control Flow

if Expressions

Blocks of code associated with the conditions in if expressions are sometimes called arms, just like the arm in match expressions. Optionally, we can also include an else expression (or else if). It's also worth noting that the condition must be a bool.


#![allow(unused)]
fn main() {
fn eight() -> i32 {
	if true {
		8
	} else {
		4
	}
}

println!("{}", eight()); // => 8
}

Using if in a let statement

Because if is an expression, we can use it on the right side of a let statement. Remember that blocks of code evaluate to the last expression in them. This means the values that have the potential to be results from each arm of the if must be the same type.


#![allow(unused)]
fn main() {
let condition = true;
let number = if condition {
	5
} else {
	6
};

println!(number is "{}", number);
}

Repetitions

Rust has three kinds of loops: loop, while and for.

Loops


#![allow(unused)]
fn main() {
loop {
	println!("again!");
}
}

You might need to pass the result of a loop. To do this, add the value you want returned after the break expression, you use to stop the loop.


#![allow(unused)]
fn main() {
let mut counter = 0;

let result = loop {
	counter += 1;

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

Conditional Loops with while

It's often useful for a program to evaluate a condition within a loop. While the condition is true, the loop runs.


#![allow(unused)]
fn main() {
let mut number = 3;

while number != 0 {
	number = number - 1;
}
}

Looping Through a Collection with for

You can use a for loop and execute some code for each item in a collection. The safety and conciseness of for loops make them the most commonly used loop construct in Rust.


#![allow(unused)]
fn main() {
for number in (1..4).rev() {
	println("{}!", number);
}
}

.rev() reverses the range

Loop label

If you have loops within loops, break and continue apply to the innermost loop at that point. You can optionally specify a loop label on a loop that we can then use with break or continue to specify that those keywords apply to the labeled loop instead of the innermost loop.


#![allow(unused)]
fn main() {
let mut count = 0;

'counting_up: loop {
	let mut remaining = 10;

	loop {
		if remaining == 9 {
			break;
		}
		if count == 2 {
			break 'counting_up;
		}
		remaining -= 1;
	}

	count += 1;
}
}

The match Control Flow Operator

Rust has an extremely powerful control flow operator called match that allows you to compare a value against a series of patterns and then execute code based on which pattern matches. The power of match comes from the expressiveness of the patterns and the fact that the compiler confirms that all possible cases are handled.

When the match expression executes, it compares the resulting value against the pattern of each arm, in order.

The code associated with each arm is an expression, and the resulting value of the expression in the matching arm is the value that gets returned for the entire match expression.

Curly brackets typically are not used if the match arm code is short.

Another useful feature of match arms is that they can bind to the parts of the values that match the pattern. This is how we can extract values out of enum variants.

Combining match and enums is useful in many situation. You will see this pattern a lot in Rust code: match against an enum, bind a variable to the data inside, and then execute code based on it.


#![allow(unused)]
fn main() {
struct Number {
	odd: bool,
	value: i32,
}

let one = Number { odd: true, value: 1 };
let two = Number { odd: false, value: 2 };

print_number(one); // => Odd number: 1
print_number(two); // => Even number: 2

// Same as with the if pattern
fn print_number(n: Number) {
	match n {
		Number { odd: true, value } => println!("Odd number: {}", value),
		Number { odd: false, value } => println!("Even number: {}", value),
	}
}
}

One requirement for match expressions is that they need to be exhaustive. At least one arm needs to match. One way to ensure you have covered every possibility is to have a catchall pattern for the last arm. A particular pattern _ will match anything, but it never binds to a variable.


#![allow(unused)]
fn main() {
fn print_number(n: Number) {
	match n.value {
		1 => println!("One"),
		2 => println!("Two"),
		_ => (),
	}
}
}

The () is just the unit value, so nothing will happen in the _ case here.

Pattern syntax

Literals


#![allow(unused)]
fn main() {
1 => println!("one")
}

Named Variables


#![allow(unused)]
fn main() {
Some(x) => println!("x = {}", x)
}

Multiple


#![allow(unused)]
fn main() {
1 | 2 => println!("one or two")
}

Range with the ..= Syntax

Ranges are only allowed with numeric values or char values, because the compiler checks that the range isn’t empty at compile time.


#![allow(unused)]
fn main() {
1..=5 => println!("one through five"),
'a'..='j' => println!("early ASCII letter"),
}

Match guard

A match guard is an additional if condition specified after the pattern in a match arm that must also match, along with the pattern matching, for that arm to be chosen. Match guards are useful for expressing more complex ideas than a pattern alone allows.

fn main() {
    let num = Some(4);

    match num {
        Some(x) if x % 2 == 0 => println!("The number {} is even", x),
        Some(x) => println!("The number {} is odd", x),
        None => (),
    }
}

@ Bindings

The at operator (@) lets us create a variable that holds a value at the same time we are testing that value to see whether it matches a pattern.


#![allow(unused)]
fn main() {
match msg {
	Message::Hello {
		id: id_variable @ 3..=7,
	} => println!("Found an id in range: {}", id_variable),
	Message::Hello { id: 10..=12 } => {
		println!("Found an id in another range")
	}
	Message::Hello { id } => println!("Found some other id: {}", id),
}
}

Concise Control Flow with if let and while let

The if let syntax lets you combine if and let into a less verbose way to handle values that match one pattern while ignoring the rest.

if let can also introduce shadowed variables in the same way that match arms can: if let Ok(age) = age.


#![allow(unused)]
fn main() {
let a = Some(40);
let b = None;

print_number(a); // => 40
print_number(b); // => No output

fn print_number(n: Option<i32>) {
	if let Some(value) = n {
		println!("{}", value)
	}
}
}

Similar in construction to if let, the while let conditional loop allows a while loop to run for as long as a pattern continues to match.


#![allow(unused)]
fn main() {
while let Some(top) = stack.pop() {
	println!("{}", top);
}
}

Concurrency

Just some ideas for research:

  • Threads
  • Parallelization (rayon create)
  • SIMD / Vectorization
    • https://github.com/rust-lang/portable-simd
    • https://doc.rust-lang.org/nightly/std/simd/index.html
    • https://ianjk.com/terrain_generator/

WebAssembly

In this book, while talking about Wasm, I will target web browsers by default. If otherwise, it will be explicitely stated that we will target any or a specific runtime.

Short History of Rust

  • 2015: Annonced
  • March 2017: First release
  • November 2017: Support "in all major browsers"
  • December 2019: Recommendation from the W3C
  • 2019: Mozilla introduced its WebAssembly System Interface (WASI)
  • June 2019: WebAssembly threads (Chrome 75)
  • May 2022: 93% of installed browsers support WebAssembly

Why WebAssembly?

Wasm (WebAssembly short name) is the little brother of asm.js. Wasm is a low-level assembly-like language with a compact binary format that runs with near-native performance. While the first implementations have landed in web browsers, there are also non-browser implementations for general-purpose use, including Wasmtime, Wasmer or WebAssembly Micro Runtime (WAMR), wasm3, WebAssembly Virtual Machine (WAVM), and many others.

Rust Setup

Crate setup

You must define properly the crate-type. The cdylib provides interoperability with C code.
In addition, rlib indicates that a Rust library will be produced.

Please refer to Chapter 3 to know more about packages, crates and modules.

# Cargo.toml

[lib]
crate-type = ["cdylib", "rlib"]

Useful Crates

  • console_error_panic_hook: This crate provides better debugging of panics by logging them with console.error. This is great for development, but requires all the std::fmt and std::panicking infrastructure, so isn't great for code size when deploying.
  • wee_alloc: This is a tiny allocator for wasm that is only ~1K in code size compared to the default allocator's ~10K. It is slower than the default allocator, however.
  • wasm-bindgen, wasm-bindgen-futures: Easy support for interacting between JS (Promises) and Rust (Futures).
  • log, console_log: A logging facility that routes Rust log messages to the browser's console.
  • web-sys, js-sys: Bindings for WEB APIs and all JS global objects and functions.
  • gloo: A modular toolkit for Rust and WebAssembly. Wrappers around web-sys and js-sys.

Cargo.toml

[package]
name = "wasm_crate"
version = "0.1.0"
edition = "2021"
publish = false

[lib]
crate-type = ["cdylib", "rlib"]

[features]
default = ["console_error_panic_hook", "wee_alloc", "console_log"]

[dependencies]
console_error_panic_hook = { version = "0", optional = true }
wee_alloc = { version = "0", optional = true }
wasm-bindgen = { version = "0", features = ["serde-serialize"] }
wasm-bindgen-futures = "0"
js-sys = "0"
web-sys = "0"
log = "0"
console_log = { version = "0", features = ["color"], optional = true }

[dev-dependencies]
wasm-bindgen-test = "0"

src/lib.rs

use log::{info, Level};
use wasm_bindgen::prelude::*;

#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

#[wasm_bindgen(start)]
pub fn main_wasm() -> Result<(), JsValue> {
	#[cfg(feature = "console_error_panic_hook")]
	console_error_panic_hook::set_once();

	#[cfg(feature = "console_log")]
	console_log::init_with_level(Level::Trace).expect("error initializing log");

	Ok(())
}

Rust and JavaScript

wasm-bindgen

In order to expose a Rust function to the outside world, you need to use the #[wasm_bindgen] macro.

#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
	a + b
}

JS Snippets

To include a local JS file, you'll use the #[wasm_bindgen(module)] macro.

Default with JS module

// js/foo.js

export function add(a, b) {
  return a + b;
}
#[wasm_bindgen(module = "/js/foo.js")]
extern "C" {
    fn add(a: u32, b: u32) -> u32;
}

Async with JS namespace

window.__extern__ = {
	async_add: (a + b) => Promise.resolve(a + b);
	// or
	async_add: async (a + b) => a + b;
}
#[wasm_bindgen(js_namespace = __extern__)]
extern "C" {
	#[wasm_bindgen(catch)]
	async fn async_add(a: u32, b: u32) -> Result<JsValue, JsValue>;
}

You can also call any function declared in the window global scope but it is not recommended if you expose a lot of function.

Crates and Modules

Rust has a specific way of declaring things.

By default, Rust brings only a few types into the scope of every program in the prelude. If a type you want to use is not in the prelude, you have to bring that type into scope explicitly with a use statement.

Hierarchy

The double colon :: is an operator that allows us to namespace this particular from function under the String type.


#![allow(unused)]
fn main() {
String::from("Hey");
}

Crate

A crate may contain a lot of modules. A crate is a folder where, in its root, you have a Cargo.toml file.

You can compare it to package.json in the JavaScript world, although we used to call it a node module. You import it with a single name, the node module name.

Modules

  • A module is a file.
  • Or a folder with a mod.rs file inside.

In the JavaScript world this is a module, or an ES module. This is when you import a file with a relative path instead of a name.

Functions

Every module exports functions.

Usage


#![allow(unused)]
fn main() {
use std::cmp::min(3, 8); // => 3
use std::cmp::*
}

Types are namespace too


#![allow(unused)]
fn main() {
let x = "rust".len(); // => 4
let x = str::len("rust"); // => 4
}

Prelude

Rust inserts this at the beginning of every module.


#![allow(unused)]
fn main() {
use std::prelude::v1::*;
}

So that, instead of std::vec::Vec::new(), you can use Vec::new().

Useful Crates

This list is obviously very opinionated and not complete. It's a list a useful crates I discovered while using Rust. Some better-known crates are not displayed below, because I prefer the ones listed here.

General purpose

Crate NameDescription
futuresFutures and streams featuring zero allocations, composability, and iterator-like interfaces.
itertoolsExtra iterator adaptors, iterator methods, free functions, and macros.
lazy_staticA macro for declaring lazily evaluated statics in Rust.
rayonSimple work-stealing parallelism for Rust.
serdeA generic serialization/deserialization framework.
serde_jsonA JSON serialization file format.
tokioAn event-driven, non-blocking I/O platform for writing asynchronous I/O backed applications.

Math, Physics, Geo, Bio

Crate NameDescription
bioA bioinformatics library for Rust.
geoGeospatial primitives and algorithms.
nalgebraGeneral-purpose linear algebra library with transformations and statically-sized or dynamically-sized matrices. Dimforge
numA collection of numeric types and traits (bigint, complex, rational, range iterators, generic integers...)
parry2d2 dimensional collision detection library in Rust. Dimforge
randRandom number generators and other randomness functionality.

String, Encoding, Time and Crypto

Crate NameDescription
chronoDate and time library for Rust.
cryptoResources for building cryptosystems in Rust using the RustCrypto project's ecosystem.
data-encodingData-encoding functions like base64, base32, and hex.
fluentA localization system designed to unleash the entire expressive power of natural language translations.
regexAn implementation of regular expressions for Rust.
unicode-segmentationGrapheme Cluster, Word and Sentence boundaries according to Unicode Standard Annex #29 rules.
uuidA library to generate and parse UUIDs.

Files

Crate NameDescription
dotenvyA dotenv implementation for Rust.
roxmltreeRepresent an XML as a read-only tree.
ronRusty Object Notation
tomlA native Rust encoder and decoder of TOML-formatted files and streams.

CLI

Crate NameDescription
clapCommand Line Argument Parser for Rust.
consoleA Rust console and terminal abstraction.
logA Rust library providing a lightweight logging facade.

Database

Crate NameDescription
slabPre-allocated storage for a uniform data type.
sqlxAn async SQL crate. Supports PostgreSQL, MySQL, and SQLite.

Graphics and UI

Crate NameDescription
imageImaging library written in Rust. Provides basic filters and decoders for the most common image formats.
pietAn abstraction for 2D graphics.
piet-svgSVG backend for piet 2D graphics abstraction.

Web

Crate NameDescription
async-graphqlA GraphQL server library implemented in Rust.
axumWeb framework that focuses on ergonomics and modularity.
hyperA fast and correct HTTP library.
jsonwebtokenCreate and decode JWTs in a strongly typed way.
lettreEmail client
reqwestHigher level HTTP client library.
towerTower is a library of modular and reusable components for building robust clients and servers.
tower-httpTower middleware and utilities for HTTP clients and servers.
tungsteniteAsync binding for Tungstenite, the Lightweight stream-based WebSocket implementation.

WebAssembly

Crate NameDescription
console_error_panic_hookA panic hook for wasm32-unknown-unknown that logs panics to console.error.
js-sysBindings for all JS global objects and functions in all JS environments.
piet-webWeb canvas backend for piet 2D graphics abstraction.
wasm-bindgenEasy support for interacting between JS and Rust.
wasm-bindgen-futuresBridging the gap between Rust Futures and JavaScript Promises.
wasm-bindgen-rayonAdapter for using Rayon-based concurrency on the Web.
web-sysBindings for all Web APIs.
wee_allocThe Wasm-Enabled, Elfin Allocator.

Rustup

rustup installs The Rust Programming Language from the official release channels, enabling you to easily switch between stable, beta, and nightly compilers and keep them updated. It makes cross-compiling simpler with binary builds of the standard library for common platforms. And it runs on all platforms Rust supports.

rustup is a toolchain multiplexer. It installs and manages many Rust toolchain and presents them all through a single set of tools installed to ~/.cargo/bin. The rustc and cargo executables installed in ~/.cargo/bin are proxies that delegate to the real toolchain. rustup then provides mechanisms to easily change the active toolchain by reconfiguring the behavior of the proxies.

This is similar to Ruby's rbenv, Python's pyenv, or Node's nvm.

See more on the rustup documentation.

Cargo

Cargo is the Rust package manager. Cargo downloads your Rust package's dependencies, compiles your packages, makes distributable packages, and uploads them to crates.io, the Rust community’s package registry.

See more on the cargo documentation

You can use cargo new or cargo init to start a new project.

Running and Building

cargo run is probably the first command you want to use in order to see the output of your software. It runs a binary or example of the local package. All the arguments following the two dashes (--) are passed to the binary to run.

cargo build compiles local packages and all of their dependencies.

Checking and Linting

cargo check checks a local package and all of its dependencies for errors. This will essentially compile the packages without performing the final step of code generation, which is faster.

cargo fix automatically takes rustc's suggestions from diagnostics like warnings and apply them to your source code. This is intended to help automate tasks that rustc itself already knows how to tell you to fix!

Clippy

Clippy is a linter which is more features complete than what the compiler can provide with cargo check or cargo fix.

It is usually installed thanks to rustup and you can lint your code with cargo clippy. In order to apply the fix, run cargo clippy --fix

Usually Clippy is well integrated into the IDE but needs to be activated. As you can imagine it needs more power and time to run properly, but believe me, it's an incredible tool.

See more on the Clippy GitHub repository

Extend Cargo

Cargo can be extended via plugin. The most useful one is probably cargo-edit which allow you to add, remove, and upgrade dependencies by modifying your Cargo.toml file from the command line. You can simply install it by running cargo install cargo-edit.

See more on the cargo-edit crate page.

Other

Cargo can also run examples, do testing, create documentation, do some benchmarking and even more. Some of those possibilities are detailed in the next chapters.