02 Apr 2023
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.
10 Mar 2023
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.
08 Mar 2023
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 String
s 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
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"),
}
}
25 Feb 2023
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
.
04 Feb 2023
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
It is also possible to access the enum fields by replacing the hard-coded value with a variable: