André Torres logger.debug("welcome")

Notes on Rust: Generics and Lifetimes

Rust like most languages, has generics. They work almost the same, with the <T> notation and you can add to functions, structus, enums and impl and traits.

// Generic in a function declaration
fn largest<T>(list: &[T]) -> &T {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }
}

// Generic in a struct
struct Point<T> {
    x: T,
    y: T,
}

// You can have a generic in a impl block, but you need the <T> twice
impl<T> Point<T> {
    fn flip_values(self) -> Point<T> {
		     Point { x: self.y, y: self.x }
    }
}

// Generic in a enum
enum Result<T, E> {
    Ok(T),
    Err(E),
}

fn main() {
    // Rust can infer the type of the genric
    let point_int = Point { x: 15, y: 15 };
    let point_float = Point { x: 15.0, y: 5.0 };
    // But the same generic type can't hold two different values.
    let point_mixed = Point { x: 15.0, y: 33 };
    println!("Hello, world!");
}

You can have methods that are only available only when you use a certain type in the generic

impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

Trait: Defining Shared Behaviour

In case you need multiple implementations of the same interface, you can use traits. Which declare an interface that can be implemented by other structs.

pub trait Summary {
		fn summarize(&self) -> String;
}

Now this trait can be implemented by multiple structs

pub struct NewsArticle {
    pub headline: String,
		pub location: String,
		pub author: String,
		pub content: String,
}

impl Summary for NewsArticle {
		fn summarize(&self) -> String {
				format!("{}, by {} ({})", self.headline, self.author, self.location)
		}
}

pub struct Tweet {
		pub username: String,
		pub content: String,
		pub reply: bool,
		pub retweet: bool,
}

impl Summary for NewsArticle {
		fn summarize(&self) -> String {
				format!("{}: {})", self.username, self.content)
		}
}

fn main() {
		let tweet = Tweet {
				username: String::from("horse_ebooks"),
				content: String::from("of course, as you probably already know, people"),
				reply: false,
				retweet: false,
		}

		println!("1 new tweet: {}", tweet.summarize());
}

You can also have a default implementation for a trait so the methods don’t have to implement the same code over and over.

impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

You can use traits as parameters and also combine traits in the parameter or generic type.

// Trait in the parameter
pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

// Having a trait in the generic
pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

// Combine traits in the paramter
pub fn notify(item: &(impl Summary + Display)) {};

// Combine traits in the generic
pub fn notify<T: Summary + Display>(item: &T) {}

// Traits can also be used as return value
fn returns_summarizable() -> impl Summary {
    Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    }
}

In case you have some complex types you don’t have to put everything in <T> part of the function. There’s a where keyword allowing to add traits

fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{}

Lifetimes

Lifetimes is a Rust concept that tracks how long a variable lives, this is to avoid dangling references. In case you try to use a value that doesn’t live long enough, the compilation will fail.

In cases where you are trying to return a reference like:

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

You will get a lifetime error

error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = 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
  |
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `lifetimes` due to previous error

The error shows how to use lifetimes in the help part. It’s the weird 'a syntax and the lifetime is generic based on the input. Then the function is updated and:

The longest string is abcd

Lifetime annotations are meant to tell Rust how generic lifetime parameters of multiple references relate to each other. What exactly the 'a in the function means? It means that the result will only live while x and y are valid, the moment they go out of scope, the result will also go out of scope. You can not specify a lifetime to a variable that you created inside the function.

It is also possible to have Lifetime annotations in structs.

struct ImportantExcerpt<'a> {
    part: &'a str,
}

In this case ImportantExcerpt can’t live more than the reference that was used to create it. In the case where:

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
		let first_sentence = novel.split(".").next().expect("Could not find a '.'");
		let i = ImportantExcerpt {
			part: first_sentence
		}
}

The ImportantExcerpt declared in i can’t outlive the novel string.

Now if you want to implement methods for ImportantExcerpt you will need to declare the lifetime

impl<'a> ImportantExcerpt<'a> {
		fn level(&self) -> i32 {
				3
		}
}

and you can mix lifetimes, generic types and trait bounds all together

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Lifetime Elision

When Rust was starting, many of the devs using Rust would end up typing the same lifetime rules over and over. The Rust team saw that and decided to apply some of the rules to the compiler, so in some cases is possible to have a reference as a parameter and result without having to explicitly mention the lifetime.

For example:

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

The compiler is able to understand that the return is a reference based on the parameter lifetime by applying a set of rules.

Static Lifetime

There’s a special lifetime called static. Declaring a value with this lifetime means that it will live for the application’s entire lifetime. Even there are some recommendations of when to use 'static. think if your reference actually lives the entire lifetime of your program.

Notes on Rust: Error Handling

There’s no way to run away from errors, eventually they will happen. Usually there are two kinds of errors:

  • Recoverable: Is an error where you can do something about it and you want to continue with the execution
  • Unrecoverable: Is an error where you can’t do anything about it and the application just has to stop.

Rust has ways to deal with both.

panic! at the program

First lets talk about unrecoverable errors. Your application might be in a state where you want to stop everything, for that you can use the panic! macro. This will cause the application to stop and print the Backtrace (in Rust is called Backtrace, but you probably hear as stacktrace) showing the calls.

We have the following program that will panic:

fn main() {
    panic!("Something wrong is not right");
}  

When we try cargo run that we get

Compiling panic v0.1.0 (rust-book/panic)
    Finished dev [unoptimized + debuginfo] target(s) in 1.27s
     Running `target/debug/panic`
thread 'main' panicked at 'Something wrong is not right', src/main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

It shows the message that I passed to the panic! macro and the location where it happens. Of course that the entire code won’t be at the main function.

fn main() {
    will_call_someting_that_panics();
}

fn will_call_someting_that_panics() {
    will_panic();
}

fn will_panic() {
    panic!("Something wrong is not right");
}

But we still get the same message when we run cargo run:

Compiling panic v0.1.0 (rust-book/panic)
    Finished dev [unoptimized + debuginfo] target(s) in 0.84s
     Running `target/debug/panic`
thread 'main' panicked at 'Something wrong is not right', src/main.rs:10:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

So we add the RUST_BACKTRACE=1 that the note mentions and:

Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/panic`
thread 'main' panicked at 'Something wrong is not right', src/main.rs:10:5
stack backtrace:
   0: rust_begin_unwind
             at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/std/src/panicking.rs:584:5
   1: core::panicking::panic_fmt
             at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/panicking.rs:142:14
   2: panic::will_panic
             at ./src/main.rs:10:5
   3: panic::will_call_someting_that_panics
             at ./src/main.rs:6:5
   4: panic::main
             at ./src/main.rs:2:5
   5: core::ops::function::FnOnce::call_once
             at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

Now we know the path that was made to get in there, but the RUST_BACKTRACE gives a way smaller message in case you are using a release build like RUST_BACKTRACE=1 cargo run --release:

Compiling panic v0.1.0 (rust-book/panic)
    Finished release [optimized] target(s) in 0.14s
     Running `target/release/panic`
thread 'main' panicked at 'Something wrong is not right', src/main.rs:10:5
stack backtrace:
   0: rust_begin_unwind
             at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/std/src/panicking.rs:584:5
   1: core::panicking::panic_fmt
             at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/panicking.rs:142:14
   2: panic::main
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

Unwinding and cleaning after yourself

After panic! Rust will execute a unwind process, which will go back and free all the memory and resources taken. This is important to avoid memory leaks or cause issues with dealing with resources like files or sockets.

In the Rust Book they mention that you can remove the unwind process to make the application smaller by changing the Cargo.toml.

[profile.release]
panic = 'abort'

Recoverable Errors

You don’t have to panic! about everything, some errors are recoverable. The file you read isn’t there? the api call failed? That’s all fine and you can move on with life using the Result<T, E> construct. Very much like the Option<T>, this is an enum where a possible failure is wrapped. The declaration of Result<T, E> is:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

In this case there’s is the generic type E so we can also pass the error. So when you try to read a file you can do:

use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    match greeting_file_result {
        Ok(_) => println!("File was read"),
        Err(error) => println!("There was an error reading file: {:?}", error),
    }

    println!("But there's not reason to panic! about")
}

Since the file does not exists you will get

Finished dev [unoptimized + debuginfo] target(s) in 0.14s
     Running `target/debug/panic`
There was an error reading file: Os { code: 2, kind: NotFound, message: "No such file or directory" }
But there's not reason to panic! about

To narrow on the error type is also possible to check the kind() function to get the specific error that it was returned.

use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    match greeting_file_result {
        Ok(_) => println!("File was read"),
        Err(error) => match error.kind() {
            std::io::ErrorKind::NotFound => println!("File not found"),
            anything_else => panic!("Something is wrong {:?}", anything_else),
        },
    }
    println!("But there's not reason to panic! about")
}

Syntactic Sugar

Now, you don’t have to use match every time you deal with result. Instead there are a set of functions it can be used that will panic the application if that’s what you want.

unwrap_or_else: it does sounds like a threat but this just adds a way to get the value or do something with the error

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {:?}", error);
            })
        } else {
            panic!("Problem opening the file: {:?}", error);
        }
    });
}

unwrap: It gives you the value or panic! the application, makes it shorter when you want to panic! on error.

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap();
}

expect: It’s just like unwrap but you can pass a nice error message to it. Its usually preferred since it makes easier to understand what’s going on.

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")
        .expect("hello.txt should be included in this project");
}

Simplifying calls with ?

The Result<T, E> enum is very useful to handle recoverable errors, and we have a lot of recoverable errors in the day to day. Think of the following example:

  • Parse a string to json
  • Validate the JSON shape
  • Get a value from it
  • make a database call
  • add a value based on the json + db data into a queue.

Every single of those steps can have errors, so we would have to use result in them. The code for that is lengthy and repetitive, look on how do_work ends becoming the same thing over and over:

enum PaymentType {
    CASH,
    CREDIT,
    DEBIT,
}

struct Payment {
    amount: isize,
    payment_type: PaymentType,
    user_id: usize,
}

struct PaymentError {
    reason: String,
}

fn main() {
    let valid_data = String::from("{}");
    match do_work(valid_data) {
        Ok(()) => println!("Success!"),
        Err(err) => println!("Error: {err}"),
    };

    let invalid_data = String::from("invalid");

    match do_work(invalid_data) {
        Ok(()) => println!("Success!"),
        Err(err) => println!("Error: {err}"),
    };
}

fn parse_json(json: String) -> Result<Payment, String> {
    if json.starts_with("{") {
        return Ok(Payment {
            amount: 150,
            payment_type: PaymentType::CASH,
            user_id: 42,
        });
    }
    return Err(String::from("Could not parse json"));
}

fn get_user_email(user_id: usize) -> Result<String, String> {
    if user_id == 42 {
        return Ok(String::from("email@example.org"));
    }
    return Err(String::from("Could not find user email"));
}

fn send_confirmation_email(email: String) -> Result<(), String> {
    println!("Will send email to {email}");
    return Ok(());
}

fn do_work(data: String) -> Result<(), String> {
    let payment = match parse_json(data) {
        Ok(payment) => payment,
        Err(err) => return Err(err),
    };

    let user_email = match get_user_email(payment.user_id) {
        Ok(email) => email,
        Err(error) => return Err(error),
    };

    match send_confirmation_email(user_email) {
        Ok(()) => println!("Confirmation email sent"),
        Err(err) => return Err(err),
    };

    return Ok(());
}

There is the ? operator that allow us to chain those operations so we can work with the success values sequentially. Let’s refactor do_work to use that

fn do_work(data: String) -> Result<(), String> {
    let user_id = parse_json(data)?.user_id;
    let user_email = get_user_email(user_id)?;
    send_confirmation_email(user_email)?;
    return Ok(());
}

Both versions of do_work will have the same result but the second one is way easier to deal with it. The ? operator propagates the Err to the caller, so we don’t have to deal with that, it’s almost like a throw but more behaved.

Notes on Rust: Collections

Rust has two types of collections:

  • Stored in the stack: Like array and tuples, you need to know the sizes of those collections so Rust can allocate the right amount of memory.
  • The ones stored in the heap: The point of those are to be with dynamic length, so you don’t need to know their size beforehand. The main ones are vector, string and hash map.

Vector

The first one is the vector, which is a dynamic length list, where you can add or remove values. To instantiate a new vector you do:

// When using Vec::new() we need to declare the type
// because it is a generic function and
// the type can't be inferred automatically
let v: Vec<i32> = Vec::new();

// In case you already have an initial value you can use vec!
// which will create a vector with the values passed and it will
// also infer the type for you
let v = vec![1, 2, 3];

Even those dynamic data structures are immutable, so if you want to add items to it you need to declare them as mutable.

// This will throw an error
let immutable_vector: Vec<i32> = Vec::new();

immutable_vector.push(1); // error[E0596]: cannot borrow `immutable_vector` as mutable, as it is not declared as mutable

// This is allowed
let mut mutable_vector: Vec<i32> = Vec::new();

mutable_vector.push(1);
mutable_vector.push(2);
mutable_vector.push(3);

Memory safety

Now, if you are storing something in a vector, you probably want to read it. There are two ways of doing data.

The unsafe way using &vector[index] that gives you a reference of the value:

let v: Vec<i32> = vec![1, 2, 3, 4, 5];
let id = &v[0];
println!("Something {}", id); // Something 1

let invalid_index = &v[10];
println!("Somethin else {}", invalid_index); // thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:3:15

There’s also the safe way with vector.get(index) that will return an Option<&T>.

let v: Vec<i32> = vec![1, 2, 3, 4, 5];
let id = v.get(1);
match id {
   Some(id) => println!("The selected element is {}", id), 
   None => println!("No valid element was selected"),
} // The selected element is 2

let invalid_element = v.get(20);
match invalid_element {
   Some(invalid_element) => println!("The selected element is {}", invalid_element),
   None => println!("No valid element was selected"),
} // No valid element was selected

Now on Vectors and Safety, when you get a value from the value from a vector, you get all the borrow checks and memory ownership safeguard that rust has.

When you have the ownership of a value from the vector you can’t modify the same.

fn main() {
	let mut v: Vec<i32> = vec![1, 2, 3, 4, 5];
  let value = &v[1]; // &v[1] or v.get(1) will enforce the rules
  v.push(6);
  println!("The element at 1 is {}", value);
}

When you try to compile, you will receive:

23 |     let value = &v[1];
   |                  - immutable borrow occurs here
24 |     v.push(6);
   |     ^^^^^^^^^ mutable borrow occurs here
25 |     println!("The element at 1 is {}", value);
   |                                        ----- immutable borrow later used here

Also when a vector is dropped from scope the values are also dropped. The borrow checker will make sure that no one is using a value.

Finally you can easily iterate over vectors too

let v = vec![100, 32, 57];
for i in &v {
	println!("{i}");
}

String

Rust has two types of string types. The &str and the String, the first one is stored in the stack and have fixed size. The other one is a dynamic one, where you can manipulate it. Here I will only talk about the second one.

We create those strings doing the following

let empty_string = String::new()
let test_string = String::from("Test");
let from_string = "from string".to_string();

You can update Strings if they are mutable

let mut s1 = String::from("foo");
let s2 = "bar";
s.push_str("_");
s.push_str(s2);
s.push('!'); // String.push() accepts a character
println!("s1 is {s1}"); // s1 is foo_bar

Syntax sugar with + and format!

You can combine strings using +, but you need to understand how the ownership will affect the variables.

let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2;
println!("s1 is {s1}, s2 is {s2}, Value of s3: {s3}");

This will throw the following error during compilation

error[E0382]: borrow of moved value: `s1`
 --> src\main.rs:5:22
  |
2 |     let s1 = String::from("Hello, ");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = String::from("world!");
4 |     let s3 = s1 + &s2;
  |              -- value moved here
5 |     println!("s1 is {s1}, s2 is {s2}, Value of s3: {s3}");
  |                      ^^ value borrowed here after move
  |

You can’t have &str + &str because both are immutable. So in case you try to concatenate them

--> src\main.rs:4:18
  |
4 |     let s3 = &s1 + &s2;
  |              --- ^ --- &String
  |              |   |
  |              |   `+` cannot be used to concatenate two `&str` strings
  |              &String
  |
  = note: string concatenation requires an owned `String` on the left
help: remove the borrow to obtain an owned `String`
  |
4 -     let s3 = &s1 + &s2;
4 +     let s3 = s1 + &s2;
  |

So if you use the + the ownership will be passed to the new variable. You can also use format! to concatenate strings, with the advantage of not having to pass the ownership to the new value

fn main() {
    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");

    let s = format!("{s1}-{s2}-{s3}");
    println!("Value of s is {s}"); // Value of s is tic-tac-toe
	  println!("s1 is {s1}, s2 is {s2}, s3 is {s3}"); // s1 is tic, s2 is tac, s3 is toe
}

Slicing and Iterating

You can’t use [] to get a single character from a String but you can use to get a slice of a String with &var[0..10].

let text = "a string with some text";
let slice = &text[0..8];
println!("Sliced string is: {slice}"); // Sliced string is a string

You can also iterate other the over the string with .chars() or .bytes()

let text = "a string with some text";
for c in text.chars() {
    print!("{c}");
}
println!("");
for b in text.bytes() {
    println!("{b}");
}

This will output to

a string with some text
9732115116114105110103321191051161043211511110910132116101120116

UTF-8 and why strings are not so simple.

Rust decided to favour safety over abstracting the complexity. Which makes quite different to work with strings in Rust, so I recommend reading

Storing UTF-8 Encoded Text with Strings - The Rust Programming Language

Hashmaps

HashMaps are to store data based on keys. This is how we can use the HashMap:

// You need to import from the collections
use std::collections::HashMap;

fn main() {
    // You need to create a mutable HashMap if you 
    // want to add anything to it
    let mut scores = HashMap::new();

    // Inserting the values to the HashMap
    // The key and value must be the same for all values
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

    // Iterating over the HashMap
    for (key, value) in &scores {
        println!("{key}: {value}");
    }
}

You can easily retrieve the value from the HashMap:

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

		// get - Retrieve the value from the HashMap, this will give you an Option<&V>
    let blue_score = scores.get("Blue");
    match blue_score {
        Some(blue_score) => println!("Blue score is {blue_score}"),
        None => println!("None"),
    }
    println!("Blue score is {blue_score}"); // Blue score is 10
}

There is also some convenient method for common operations

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

		// Combine entry and or_insert to insert a value if isn't in the HashMap
    scores.entry(String::from("Red")).or_insert(30);
		let red_score = scores.get("Red");
    match red_score {
        Some(red_score) => println!("Red score is {red_score}"),
        None => println!("None"),
    }

		// You can also update the value in the same way
    let green_score = scores.entry(String::from("Green")).or_insert(20);		
    *green_score += 5;
		let green_score = scores.get("Green");		
    match green_score {
        Some(green_score) => println!("Green score is {green_score}"),
        None => println!("None"),
    }
}

Notes on Rust: Crates, Modules and Cargo

In the Rust world they use the word crate to identify a library that doesn’t have an entry point, and a binary to be something with a entry point.

Modules

Modules are the way that rust uses to organize and group code. You can declare modules using the mod keyword. The way the code is structured in the folder is also the way where you will have your crates path, but isn’t simple like creating the folder and the files then importing it, you need to create a file with the module declaration.

You can also use modules as a way to group code and control what do you expose, you have the pub modifier that makes the method public, since the default is to make everything private from it’s parents. Let’s use the restaurant example from the rust book we would need to create the following.

We have a main.rs:

pub mod front_of_house;

fn main() {
    crate::front_of_house::hosting::add_to_waitlist();
    // Relative import is also allowed
    front_of_house::hosting::add_to_waitlist();
}
// src/front_of_house.rs
pub mod hosting;
// src/front_of_house/hosting.rs
pub fn add_to_waitlist() {
    println!("Add to waitlist")
}

The file structure would be:

restaurant
├── Cargo.lock
├── Cargo.toml
└── src
    ├── front_of_house
    │   └── hosting.rs
    ├── front_of_house.rs
    └── main.rs
  • main.rs has the declaration for the front_of_house module,
  • front_of_house.rs has the declaration for the hosting module.
  • hosting.rs has the function add_to_waitlist.

Everything that has the pub keyword inside the hosting.rs file will be available to be used in the main.rs through the crate::front_of_house::hosting::add_to_wallet().

The use keyword

Specifying the full path every time you are going to call a function might not be the most ergonomic thing, to avoid that there’s the use keyword that allows you to import things to the file.

We import the hosting module to the src/main.rs file.

pub mod front_of_house;

use front_of_house::hosting;

fn main() {
    hosting::add_to_waitlist();
}

Conventions around use

Now rust has some conventions that were created with the time about how do you import things. In case of functions you should import the module and use the the module name and the function name in combination like we have in the example above. This way the reader knows that the function comes from another module.

Now for struct, enums and other values you should import the entire path. So if we add an enum to our hosting module

// src/front_of_house/hosting.rs
#[derive(Debug)]
pub enum CustomerType {
    REGULAR,
    VIP,
}

pub fn add_to_waitlist(customer_type: CustomerType) {
    println!("Add to waitlist {:?} customer", customer_type);
}

We can use the entire thing without having to do hosting::CustomerType

// src/main.rs
pub mod front_of_house;

use front_of_house::hosting;
use front_of_house::hosting::CustomerType;

fn main() {
    hosting::add_to_waitlist(CustomerType::REGULAR);
}

The only exception for this rule is when you have two objects with the same name in a file, then you need to import the module to remove that ambiguity.

use std::fmt;
use std::io;

fn function1() -> fmt::Result {
    // --snip--
}

fn function2() -> io::Result<()> {
    // --snip--
}

Another alternative is using the as keyword that allow you to rename an import.

use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
    // --snip--
}

fn function2() -> IoResult<()> {
    // --snip--
}

syntax sugar

Rust also has some unix like syntax sugar for importing so you don’t have to keep importing the same thing over and over.

The first one is using curly braces to import multiple things from the same path.

// This two imports
use std::cmp::Ordering;
use std::io;

// Can be turned into a single one
use std::{cmp::Ordering, io};
// You can use `self` to import std::io and a submodule
use std::io::{self, Write}

And there’s also a glob (*) operator in case you want to import everything that is public for that path.

use std::collections::*;

fn main() {
    let hash_map = HashMap::new()
}

When using the glob operator you might end up not knowing if is something in the scope or something that was defined by your program.

re-exporting with pub use.

Finally, you have the reexport with the pub use. This will make something that you imported available to the other packages. This is good when you want to export a different structure to the users.

mod front_of_house;

pub use front_of_house::hosting;

fn main() {
    hosting::add_to_waitlist();
}

Using cargo

Cargo is rust build system and package manager, it’s a very good tool but it has some quirks.

Multiple entry points

Sometimes you want to have different entry points for your application. I was doing the Advent of Code and wanted to have everything into a single project but run them in isolation.

Multiple bins with a bin folder

Cargo can compile and run multiple executables if you have them under the src/bin folder. So in the case were we have a code challenge every day, we can have a structure like

advent_of_code
├── Cargo.lock
├── Cargo.toml
└── src
    ├── bin
    |   ├── day_1.rs
    │   └── day_2.rs
    └── main.rs

We put the main method inside those files in the bin folder:

// src/bin/day_1.rs
fn main() {
    println!("Day 1");
}

// src/bin/day_2.rs
fn main() {
    println!("Day 2");
}

Now that we have multiple bins, we must specify the one we want to run. In this case we have three options, advent_of_code that is the root binary from src/main.rs, day_1 and day_2 that are inside src/bin.

$ cargo run --bin advent_of_code
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target\debug\advent_of_code.exe`
Hello, world!

$ cargo run --bin day_1
   Compiling advent_of_code v0.1.0 (C:\Users\andre\projects\advent_of_code)
    Finished dev [unoptimized + debuginfo] target(s) in 0.26s
     Running `target\debug\day_1.exe`
Day 1

$ cargo run --bin day_2
   Compiling advent_of_code v0.1.0 (C:\Users\andre\projects\advent_of_code)
    Finished dev [unoptimized + debuginfo] target(s) in 0.23s
     Running `target\debug\day_2.exe`
Day 2

Multiple bins within Cargo.toml

In case you don’t want to have the bin folder, there’s also the possibility to specify the binary in the Cargo.toml file. So we have the same project with a different structure

advent_of_code
├── Cargo.lock
├── Cargo.toml
└── src
    ├── day_1.rs
    ├── day_2.rs
    └── main.rs

Then in the Cargo.toml we should specify the all the three entry points:

[package]
name = "advent_of_code"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name = "day_1"
path = "src/day_1.rs"

[[bin]]
name = "day_2"
path = "src/_day_2.rs"

[dependencies]

You can run the same way using the cargo run --bin bin_name.

Notes on Rust: Tuples, Structs, Enums and Pattern Matching

Tuples

Tuples are the most basic data structure that we have in Rust. It’s based on positioning, like a fixed length array.

let first_example = get_something(String::from("First Example"));
println!("Values are: {} and {}", first_example.0, first_example.1);

// This is a way to destruct the tuple into variables for easy access
let (string_value, integer_value) = get_something(String::from("Something"));
println!("Values are: {} and {}", string_value, integer_value);

That would end printing:

Values are: First Example and 0
Values are: Something and 0

It’s also possible to add names to tuples with the struct keyword.

struct Point(i32, i32);

fn main() {
    let coordinates = Point(3, 14);
    print_coordinates(&coordinates);
}

fn print_coordinates(point: &Point) {
    println!("Coordinates x: {} and y: {}", point.0, point.1);
}

In case you name a tuple using struct you won’t be able to pass arbitrary tuples or tuples with the same shape but different names.

struct Point(i32, i32);
struct LatLong(i32, i32);

fn main() {
    let coordinates = Point(3, 14);
    print_coordinates(&coordinates);

    let lat_long = LatLong(4, 5);
    print_coordinates(&lat_long);

    print_coordinates((12, 33));
}

fn print_coordinates(point: &Point) {
    println!("Coordinates x: {} and y: {}", point.0, point.1);
}

Throws errors

error[E0308]: mismatched types
  --> src/main.rs:9:23
   |
9  |     print_coordinates(&lat_long);
   |     ----------------- ^^^^^^^^^ expected struct `Point`, found struct `LatLong`
   |     |
   |     arguments to this function are incorrect
   |
   = note: expected reference `&Point`
              found reference `&LatLong`
note: function defined here
  --> src/main.rs:14:4
   |
14 | fn print_coordinates(point: &Point) {
   |    ^^^^^^^^^^^^^^^^^ -------------

error[E0308]: mismatched types
  --> src/main.rs:11:23
   |
11 |     print_coordinates((12, 33));
   |     ----------------- ^^^^^^^^ expected `&Point`, found tuple
   |     |
   |     arguments to this function are incorrect
   |
   = note: expected reference `&Point`
                  found tuple `({integer}, {integer})`
note: function defined here
  --> src/main.rs:14:4
   |
14 | fn print_coordinates(point: &Point) {
   |    ^^^^^^^^^^^^^^^^^ -------------

For more information about this error, try `rustc --explain E0308`.
error: could not compile `structs` due to 2 previous errors

Struct

Structs are a way to group fields together into a single declaration, like an object. Structs in Rust are declared as the following:

struct User { // Name of the strcut
    username: String, // Fields
    email: String,
    active: bool,
    sign_in_count: u64, // You add the trailing comma
}

Now to instantiate a new struct you can do, no need to use new or parenthesis:

let user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};

// To declare a mutable struct just add the `mut` keyword. The entire struct is mutable, there isn't a way to only have a single field mutable
let mut user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};

To declare a mutable struct just add the mut keyword. The entire struct is mutable, there isn’t a way to only have a single field mutable.

let mut user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};

In Rust you have some syntactic sugar to instantiate structs:

  • Shorthand field: If you have a variable with the name of the field, you don’t have to put the field twice (name and value), just put the variable.
let email = String::from("someone@example.com");
let username = String::from("someusername123");
let mut user1 = User {
    email,
    username,
    active: true,
    sign_in_count: 1,
};
  • Spread Operator: You can use other structs to build a new struct. With the spread operator ...
let email = String::from("someone@example.com");
let username = String::from("someusername123");
let user1 = User {
    email,
    username,
    active: true,
    sign_in_count: 1,
};

let user2 = User {
    email: String::from("otheruser@example.com"),
    ..user1
};
println!("Testing Log {}", user2.email); // Will print "Testing Log otheruser@example.com"

Is worth to remind that the spread operator in Rust is different than in Javascript. In Javascript will spread all the fields from the object into the new one. In Rust what is done is that ONLY the missing fields are copied to the new object.

And with the spread operator, the ownership of the data is passed to the new object. So you won’t have access to the field in the original object.

let email = String::from("someone@example.com");
let username = String::from("someusername123");
let user1 = User {
    email,
    username,
    active: true,
    sign_in_count: 1,
};

let user2 = User {
    email: String::from("otheruser@example.com"),
    ..user1
};
println!("Testing Log {}", user2.email); // Will print "Testing Log otheruser@example.com"
println!("Testing Log {}", user1.email); // Will print "Testing Log someone@example.com"
println!("Testing Log {}", user1.username); // Will not compile

If you try to compile the example above the compiler will throw the following error:

warning: unused variable: `user2`
  --> src\main.rs:18:9
   |
18 |     let user2 = User {
   |         ^^^^^ help: if this is intentional, prefix it with an underscore: `_user2`
   |
   = note: `#[warn(unused_variables)]` on by default

error[E0382]: borrow of moved value: `user1.username`
  --> src\main.rs:22:32
   |
18 |       let user2 = User {
   |  _________________-
19 | |         email: String::from("otheruser@example.com"),
20 | |         ..user1
21 | |     };
   | |_____- value moved here
22 |       println!("Testing Log {}", user1.username);
   |                                  ^^^^^^^^^^^^^^ value borrowed here after move
   |
   = note: move occurs because `user1.username` has type `String`, which does not implement the `Copy` trait
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0382`.
warning: `structs` (bin "structs") generated 1 warning
error: could not compile `structs` due to previous error; 1 warning emitted

A struct also can have NO fields, to be just an empty declaration. This is called a Unit Type.

struct AlwaysEqual;

fn main() {
  let subject = AlwaysEqual;
}

Ownership of the Data

The struct should own the all the data inside, so in a struct like:

struct User {
    username: String,
    email: String,
    active: bool,
    sign_in_count: u64,
}

When the main struct is not being used anymore all the data is freed from memory, specially the username and email fields that are using a String which are variable size types that are stored in the heap.

In case we try to create a struct with references you start to bump into certain issues. So if we replace the String with &str:

struct User {
    username: &str,
    email: &str,
    active: bool,
    sign_in_count: u64,
}

The compiler will start throwing errors:

error[E0106]: missing lifetime specifier
 --> src/main.rs:5:15
  |
5 |     username: &str,
  |               ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
4 ~ struct User<'a> {
5 ~     username: &'a str,
  |

error[E0106]: missing lifetime specifier
 --> src/main.rs:6:12
  |
6 |     email: &str,
  |            ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
4 ~ struct User<'a> {
5 |     username: &str,
6 ~     email: &'a str,
  |

Right now I haven’t reached the Lifetimes part, so will stop by here.

Printing Structs

Since structs have user defined shapes, isn’t that easy to simply print a struct,

struct User {
    username: String,
    email: String,
    active: bool,
    sign_in_count: u64,
}

fn main() {
    let user = User {
        username: String::from("Username"),
        email: String::from("email@something.com"),
        active: true,
        sign_in_count: 2,
    };

    println!("Logged User: {}", user);
}

Has the compiler throwing:

error[E0277]: `User` doesn't implement `std::fmt::Display`
  --> src/main.rs:19:33
   |
19 |     println!("Logged User: {}", user);
   |                                 ^^^^ `User` cannot be formatted with the default formatter
   |
   = help: the trait `std::fmt::Display` is not implemented for `User`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

So what happens if we use {:?} or {:#?}?

error[E0277]: `User` doesn't implement `Debug`
  --> src/main.rs:19:35
   |
19 |     println!("Logged User: {:?}", user);
   |                                   ^^^^ `User` cannot be formatted using `{:?}`
   |
   = help: the trait `Debug` is not implemented for `User`
   = note: add `#[derive(Debug)]` to `User` or manually `impl Debug for User`
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider annotating `User` with `#[derive(Debug)]`
   |
4  | #[derive(Debug)]
   |

error[E0277]: `User` doesn't implement `Debug`
  --> src/main.rs:20:36
   |
20 |     println!("Logged User: {:#?}", user);
   |                                    ^^^^ `User` cannot be formatted using `{:?}`
   |
   = help: the trait `Debug` is not implemented for `User`
   = note: add `#[derive(Debug)]` to `User` or manually `impl Debug for User`
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider annotating `User` with `#[derive(Debug)]`
   |
4  | #[derive(Debug)]
   |

Well, rust still can’t print. Due it’s low level nature rust don’t have things like reflection to inspect objects at runtime. So we need to add Debug symbols at the structs we want to print. After adding the #derive(Debug) to the struct we can print:

#[derive(Debug)]
struct User {
    username: String,
    email: String,
    active: bool,
    sign_in_count: u64,
}

And here’s the cargo run result:

Logged User: User { username: "Username", email: "email@something.com", active: true, sign_in_count: 2 }
Logged User: User {
    username: "Username",
    email: "email@something.com",
    active: true,
    sign_in_count: 2,
}

Rust also has the dbg! macro that that prints the data with extra information. So we add that to our code:

let user = User {
    username: String::from("Username"),
    email: String::from("email@something.com"),
    active: true,
    sign_in_count: dbg!(2 + 1),
};

dbg!(&user)

And we get information about the method that was called and the value that returned. with the file and line.

[src/main.rs:16] 2 + 1 = 3
[src/main.rs:19] &user = User {
    username: "Username",
    email: "email@something.com",
    active: true,
    sign_in_count: 3,
}

Methods

It’s also possible to have methods in a struct, this way you can call struct.method(). The syntax to declare that is:

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

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

fn main() {
	let rectangle = Rectangle {
	  width: 13,
    height: 44,
	};
  println!("Rectangle area: {}", rectangle.area());
}

So the functions inside the impl part will become methods in the Rectangle struct. One thing that functions inside the impl block differs from regular functions is that they always have &self as the first parameter so we can have access to the struct fields.

&self is the shorthand for self: &Self which is the type of the struct that you are implementing the methods for. In this case self: &Rectangle.

You can also write functions without the &self as first parameter, they are called Associated Functions. For example String::from is a associated function, and as you can see they diverge on how they are called.

Creating a associated function:

impl Rectangle {
    fn square(size: u32) -> Self {
        return Self {
            width: size,
            height: size,
        };
    }
}

You are also allowed to have multiple implementation blocks if you want:

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

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

    fn can_hold(&self, other: &Rectangle) -> bool {
        return self.height >= other.height && self.width >= other.width;
    }
}

impl Rectangle {
    fn square(size: u32) -> Self {
        return Self {
            width: size,
            height: size,
        };
    }
}

Having multiple impl blocks will cause the functions to be merged into a single one.

Enums and Pattern Matching

In rust enums are declared with the following syntax

enum IpAddrKind {
    V4,
    V6,
}

then you can instantiate them with

let ipv4 = IpAddrKind::V4;
let ipv6 = IpAddrKind::V6;

Enums can also carry a value that you pass when you instantiate them like a struct, and different enum values can have different types of values:

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let host = IpAddr::V4(String::from(127, 0, 0, 1));
let hostV6 = IpAddr::V6(String::from("::1"));

You can also use structs and other enums as the enum value. An example of enum with multiple types of values.

struct Position {
    x: i32,
    y: i32
}

enum Message {
    Quit,
    Move(Position),
    Write(String),
    ChangeColor(i32, i32, i32),
    Image { url: String, alt_text: String },     
}

The Image has named fields like a struct would have.

Just like in structs you can implement methods to an enum with the impl block.

impl Message {
    fn call(&self) {
         // Method Implementation
    }
}

let message = Message::Write(String::from("Something"));
message.call();

Option enum

Instead of dealing with null values rust uses the Option enum, like the Null Object Pattern, you get a concrete implementation instead of a null reference and you can chose to do what you want when you have a null value.

This advantage of using the Option enum is that you can have the compiler to check exhaustively that you handled the null case avoid problems with it.

The Option enum comes with the standard library and it’s definition is:

enum Option<T> {
    None,
    Some(T),
}

You can use the None and Some values without having to put the Option::

let a_proper_value: Option<i32> = Some(15);
let an_empty_value: Option<i32> = None;

match - pattern matching with enums

Now is clear on how to declare enums, but they are not complete with a good way to use them. That’s where the match keyword comes. With match you can have an exhaustive check at compile time to be sure you took care of all the cases in your enum.

Imagine that you want to route a request to a different endpoint based on an enum value

enum Stage {
    Gamma,
    Production,
}

fn get_stage_url(stage: Stage) -> String {
    match stage {
        Stage::Gamma => String::from("https://example.org/gamma"),
        Stage::Production => String::from("https://production.org"),
    }
}

fn main() {
    println!("Using url {}", get_stage_url(Stage::Production));
}

The code above would print:

Using url https://production.org

Now this is a very simple enum, and we saw that we can have more data than just its name. match also allows to access the values inside the enum and you can even make more complex computations inside the match.

fn main() {
   let email = get_email_by_username(String::from("username"));
    match email {
        Some(email) => {
            send_email_to_user(email);
            println!("Email sent successfully");
        }
        None => println!("User don't have an email"),
    }
}

fn get_email_by_username(username: String) -> Option<String> {
    Some(format!("{}@example.org", username))
}

fn send_email_to_user(email: String) {
    println!("Email sent to {}", email);
}

Sometimes you just need to work with a small subset of values of an enum and just ignore the rest, for that you can use the _ as a default case and {} as a void function. Imagine if you were coding an Automated Vending Machine that only accepts 1 euro and 50 cents coins, you could implement the following code.

enum Coin {
    OneEuro,
    Fifty,
    TwentyFive,
    Ten,
    Five,
    Two,
}

fn main() {
    let mut total: u32 = 0;
    let inserted_coins = [Coin::OneEuro, Coin::Ten, Coin::Ten, Coin::Fifty];

    for coin in inserted_coins {
        match coin {
            Coin::OneEuro => total = total + 100,
            Coin::Fifty => total = total + 50,
            _ => {}
        }
    }

    println!("Total: {}", total);
}

Finally if you need just a single value of an enum then you can use the if let construct. If you were building an RPG and you want to add a feature that when the user rolls 19 they get a a buff. We get the input wrapped in a Option<i32> and then we have to check.

fn apply_buff() {
    println!("Buff applyed!!!");
}

fn main() {
    let first_roll = Some(5);
    let second_roll = Some(19);

    if let Some(19) = first_roll {
        apply_buff();
    }
    if let Some(19) = second_roll {
        apply_buff();
    }
}

The code above would print

Buff applyed!!!

It is also possible to access the enum fields by replacing the hard-coded value with a variable: