An open API service indexing awesome lists of open source software.

https://github.com/albertoeaf/learn-rust

Repo just to store projects to learn Rust.
https://github.com/albertoeaf/learn-rust

Last synced: about 1 year ago
JSON representation

Repo just to store projects to learn Rust.

Awesome Lists containing this project

README

          

#+SETUPFILE: https://fniessen.github.io/org-html-themes/org/theme-readtheorg.setup
#+TITLE: Learn rust
# Allow 6-level deep ToC's:
#+OPTIONS: H:6

Repo just to store projects to learn Rust.

* Notes

** Cargo

[[https://doc.rust-lang.org/book/ch01-03-hello-cargo.html][Hello cargo - using cargo]]

Use cargo to manage projects:
- ~cargo new ~ :: Create new project
- ~cargo build [--release]~ :: Build project
- ~cargo run~ :: (Build and) run project
- ~cargo check~ :: Ensure the project builds (faster than build)

** Enums and variants

[[https://doc.rust-lang.org/book/ch02-00-guessing-game-tutorial.html][Guessing game - enums and variants]]

Rust often uses types named ~Results~, which are enumerations
(enums): a type with a fixed set of values. Each value is called a
variant.

For ~Result~, variants are ~Ok~ and ~Err~.

** Ownership

[[https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html][Ownership]]

Ownership rules:
1. Every /value/ in Rust has a /variable/ that is called its /owner/.
2. There can only be one /owner/ at a time.
3. When the owner goes out of scope, the value will be dropped.

*** Move/copy
Due to move,

#+begin_src rust
let s1 = String::from("hello"); // Data allocated on heap. Ownership & borrowing comes into play.
let s2 = s1; // After this point, s1 is invalidated (moved), and its value can no longer be borrowed.

println!("{}, world!", s1); // Error: cannot borrow invalidated / moved variable values.
#+end_src

automatic copying can always be assumed to be inexpensive in terms of runtime performance. That is because by design Rust will never automatically create /deep/ copies of the data.

To /deep/-copy the data, use the commonly called ~clone~ method.

This is not needed for types with size known at compile-time like integers, that are stored entirely on the stack.
For those, as the copy is inexpensive there's no difference between deep and shallow copying, and there would be no aid in invalidating copied variables, hence ~clone~ can be left out.
Any variable in Rust with the ~Copy~ annotation (that is for instance placed on types like integers that are stored on the stack). Any type implementing the ~Copy~ trait still leaves the older variable used after assignment. Rust does not allow annotating a type with the ~Copy~ trait if the type implements the ~Drop~ trait. All this is explained [[https://doc.rust-lang.org/book/appendix-03-derivable-traits.html][Appendix C: Derivable traits]].

*** Borrowing

[[https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html][Borrowing and references]]

Borrowing is done through references. They are useful to avoid moving values back and forth.

References rules:
- References do not allow modifying borrowed values, unless they are declared as mutable.
- There can be multiple immutable references to the same value in a given scope.
- There can be only one mutable reference to a value in a given scope.
- Cannot have mutable and immutable references to the same value in a given scope. /Users of immutable references would hate for their values to suddenly change./
- Scope of a reference goes between its declaration and its last use. /This can happen earlier than the end of scope!/

The following example showcases such rules:
#+begin_src rust
let mut s = String::from("hello");

let r1 = &mut s; // Start `r1` reference scope.
let r2 = &mut s; // ERROR: two mutable references in the same block.

{
let r3 = &mut s; // Valid, no other references to `s` in this scope.
}

println!("{}", r1); // End `r1` reference scope.

let r4 = &mut s; // Valid. Scope of `r1` ended already.
#+end_src

The advantage of such restrictions is that Rust can prevent data races at compile time. A data race happens when the three behaviours occur:
- Two or more pointers access the same data at the same time.
- At least one of the pointers is being used to write to the data.
- There's no mechanism being used to synchronize access to the data.

** Slice type

[[https://doc.rust-lang.org/book/ch04-03-slices.html][Slices]]

Slices are a type that don't have ownership. They are used to reference a contiguous sequence of elements in a collection.

They are better than tracking indices to a given string variable as they are kept in sync with the string itself, hence always remain valid.

*** String slice
String slices are of type ~str~. Hence, the type ~&str~ is an immutable reference to a string slice, the type used for string literals in Rust code. Hence, ~&str~ is more generic than ~&String~ for function arguments when defining functions.

To make a function that returns a slice with the first word in a string you'd write:

#+begin_src rust
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();

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

&s[..]
}
#+end_src

*** Other slice types

String slices are only valid for strings. Other slice types are possible too:

#+begin_src rust
let a = [1, 2, 3, 4, 5];

let slice = &a[1..3]; // slice type: &[i32]

assert_eq!(slice, &[2, 3]);
#+end_src

** Structs

[[https://doc.rust-lang.org/book/ch05-01-defining-structs.html][Structs]]

Like tuples, structs serve to bundle together different pieces of data. But in structs, those members are named.

Example of struct definition:

#+begin_src rust
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
#+end_src

Example of struct creation and modification:

#+begin_src rust
let mut user1 = User {
email: String::from("someone@example.com"), // Order of fields is not relevant.
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};

user1.active = false; // Only works if variable is mutable!
#+end_src

Also, one can use *init field shorthand* to initialize struct fields with the same name of the parameter:

#+begin_src rust
fn build_user(email: String, username: String) -> User {
User {
email: email,
username, // using field init shorthand
active: true,
sign_in_count: 1,
}
}
#+end_src

Or *struct update syntax* to initialize a new struct from the values of another one:
#+begin_src rust
let user2 = User {
email: String::from("another@example.com"),
username: String::from("anotherusername567"),
..user1 // Use the remaining fields from `user1`.
};
#+end_src

*** Struct tuples

Define structs that look like tuples called *tuple structs*. These have different types according to the struct name, instead of the underlying tuple field types, which can be useful for type safety:

#+begin_src rust
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
#+end_src

*** Unit-like structs

Sometimes it is convenient to define new types that have no data, like ~Unit~. These are called *unit-like structs* and behave similarly to the unit type ~()~. This can be useful to implement new trait types for instance.

** Enums

[[https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html][Enums]]

Enums in Rust are very powerful and its variants can even carry data of different types:

#+begin_src rust
enum Message {
Quit, // No data associated.
Move { x: i32, y: i32 }, // Includes anonymous struct.
Write(String), // Includes a String.
ChangeColor(i32, i32, i32), // Includes three `i32` values.
}
#+end_src

Remember, this defines a type ~Message~, which is not the same as defining four /struct types/.

*** Option type

[[https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html][Option]]

Rust does not have *null* values, which is a frequent source of bugs, but easily allows expressing the same idea, of a value being available or missing: ~Option~. It is defined in the standard library as having the variants ~Some(T)~ and ~None~:
#+begin_src rust
enum Option { // is a generic type parameter in Rust.
Some(T),
None,
}
#+end_src

** Match control flow operator

[[https://doc.rust-lang.org/book/ch06-02-match.html][Match control flow operator]]

Rust has a very powerful ~match~ control flow operator that can do destructuring and at the same time ensure all variants are covered, making the code much more robust.

** If let

Based on the ~match~ operator, the ~if let~ syntax allows much simpler code to handle values that match a single pattern, while ignoring the rest.

Example - use match to execute some code if the value is 3:

#+begin_src rust
let some_u8_value = Some(0u8);
match some_u8_value {
Some(3) => println!("three"),
_ => (),
}
#+end_src

As ~match~ is exhaustive, the arm ~_ => ()~ had to be added. By using ~if let~, the code becomes much shorter:

#+begin_src rust
let some_u8_value = Some(0u8);
if let Some(3) = some_u8_value {
println!("three");
}
#+end_src

The idea is that the single arm is placed in the ~if let~ expression and if it is a match, the ~if then~ branch is executed. This means processing the alternatives is as simple as adding an ~else~ after the ~if~.

** Packages and crates

[[https://doc.rust-lang.org/book/ch07-01-packages-and-crates.html][Packages and crates]]

Important concepts:
- A *crate* is a binary or library.
- A *package* is one or more crates that provide a set of functionality.
- A /package/ contains a /Cargo.toml/ file which describes how to build those crates.
- If the module of the crate is either /src/lib.rs/ or /src/main.rs/, the crate becomes a *library* or *binary* crate.
- A package must contain:
- At least one crate (binary or library)
- As many binary crates as desired
- At most one library crate

*** Modules - controlling scope and privacy

[[https://doc.rust-lang.org/book/ch07-02-defining-modules-to-control-scope-and-privacy.html][Modules - Controlling scope and privacy]]

** Error handling

[[https://doc.rust-lang.org/book/ch09-00-error-handling.html][Error handling]]

Rust has no exception system, and unlike other languages divides errors between recoverable and irrecoverable.
Error handling in Rust is an extremely robust and tight system.

Recoverable errors are handled with the type ~Result~. Irrecoverable errors are thrown with the macro ~panic!~.

~Result~ has many helper methods. One of these is *~unwrap()~*:

#+begin_src rust
let f = File::open("hello.txt").unwrap(); // Return value (T) or panic! with E.
#+end_src

Alternatively, one can use ~.expect("custom error message")~ instead to specify a message in the resulting ~panic!~ macro call.

Another one is the *~?~* after a ~Result~. If the operation is an ~Ok~, the value inside it is returned. Otherwise, the error is returned and cast from that type to the current function return type - if there's a defined conversion for that. Also, if there was an Error, the current function returns it immediately.

#+begin_src rust
fn read_username_from_file() -> Result {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
#+end_src

which can be made even shorter with *chaining*:

#+begin_src rust
fn read_username_from_file() -> Result { // or simply call std::fs::read_to_string(filepath) ;)
let mut s = String::new();

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

Ok(s)
}
#+end_src

** Generic Types, Traits, and Lifetimes

[[https://doc.rust-lang.org/book/ch10-00-generics.html][Generic Types, Traits, and Lifetimes]]

*** Generics

Generics are heavily inspired on C++ syntax, take a look:

#+begin_src rust
fn larger(T x, T y) -> bool {
x > y // compiler will require trait std::cmp::PartialOrd.
}

fn main() {
let a : i32 = 1;
let b : i32 = 4;

if larger(a, b) { // Won't compile though. We must restrict to type with std::cmp::PartialOrd.
println!("Larger!");
}
}
#+end_src

but traits matter too!

Anyways, this can of course be used for types as well like custom structs.

*** Traits

[[https://doc.rust-lang.org/book/ch10-02-traits.html][Traits]], [[https://doc.rust-lang.org/book/appendix-03-derivable-traits.html][Derivable Traits]]

Traits work similarly to interfaces in other languages. A Trait is the requirement that some given set of methods are implemented.
The compiler then ensures that any type annotated with that Trait must have such behaviours defined, with those exact signatures.

Example:
#+begin_src rust
pub Trait Summary {
fn summarize(&self) -> String;
}
#+end_src

**** Implementing a Trait on a Type

Implementing a Trait on a Type is done still with an ~impl~ block, but instead of being just ~impl Classname { definitions... }~, use instead ~impl Trait for Class { implementations... }~:

#+begin_src rust
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}

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

**** Implementing methods that apply to traits

After defining traits, one can also define methods that operate on objects that implement a given trait:
#+NAME: trait_bound_sugar_syntax
#+CAPTION: Trait bound sugar syntax.
#+begin_src rust
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
#+end_src

**** Trait bounds

The code above ([[trait_bound_sugar_syntax]]) is syntax sugar for a *Trait Bound*:

#+begin_src rust
pub fn notify(item: &T) {
println!("Breaking news! {}", item.summarize());
}
#+end_src

**** Multiple trait bounds

Multiple trait bounds can be declared with ~&(impl Summary + Display)~ or where clauses:

#+begin_src rust
fn f(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug
{
...
#+end_src

Of course, ~impl~ can also be used for return types, say ~-> impl Summary~.

However, the function must always return the same type of object that does implement that trait,
and not several different types that happen to implement that trait. For that see the appendix [[https://doc.rust-lang.org/book/ch17-02-trait-objects.html#using-trait-objects-that-allow-for-values-of-different-types][Using Trait Objects That Allow for Values of Different Types]].

/Also, instead of conditionally implementing a trait for any trait that implements another trait, implementations of traits on a type that satisfies the trait bounds are called *blanket implementations*./
For instance, the standard library implements the ~ToString~ trait on any type that implements the ~Display~ trait:
#+begin_src rust
impl ToString for T {
...
}
#+end_src

that's the reason why we can run ~let s = 3.to_string();~, because integers implement ~Display~.

** Validating references with lifetimes

[[https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html][References and lifetimes]]

Like types, lifetimes are inferred in most cases. Only when the lifetime could be ambiguous are we required to specify it.
Lifetimes ensure the actual references used at runtime are valid.

#+begin_src rust
{
let r;

{
let x = 5;
r = &x;
}
// Compilation error here because x's lifetime is over, and r would reference a value that is no longer in scope.

println!("r: {}", r);
}
#+end_src

The compiler determines if the lifetimes are valid through use of a *borrow checker*.

/Also: Rust does not allow null values. Trying to use a variable (like ~r~) before it having a value would result in a compile-time error./

*** Lifetime annotation syntax

Explicit lifetime annotations serve not to modify how long any reference lives, but to tell the borrow checker relationships/constraints between different references' lifetimes.

The name of a lifetime parameter must start with an apostrophe ='= and is usually very short, like ~'a~:
#+begin_src rust
&i32 // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
#+end_src

To define equal lifetimes for two variables where both live as long as each other (~'a~) and for the return value for a function that receives two parameters by reference, thus ensuring that even the return is never invalidated (same lifetime as the inputs) would go like this:
#+begin_src rust
fn longest_string<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
#+end_src

As *lifetime specifications do not modify lifetimes*, in practice this means the *borrow checker will only consider the returned reference valid as long as the smallest of both input references lifetimes*, the only satisfiable way for the compiler to obey such lifetime constraints at compile-time in all possible scenarios[fn:Using only type information. See example in manual where a human could look at the constants and know that when the program runs only one of the branches of the program executes, and hence, the borrow checker will be more "strict" than absolutely needed and reject the program when it was acceptable.].

The borrow checker is smart enough to also prevent returning references to internal function variables in stack, as they would go out of scope when the function returned. This all means that dangling references are *impossible* in Rust

*** Lifetime ellision

As time passes more lifetime ellision rules (patterns hard-coded into Rust's compiler) might emerge that avoid forcing the user to specify the lifetimes.

*** Static lifetime

There's also the ~'static~ lifetime which is meant for the duration of the program. All string literals have the ~'static~ lifetime since their values are hard-coded in the program's binary, which is always available.

** How to write tests

[[https://doc.rust-lang.org/book/ch11-01-writing-tests.html][How to Write Tests - The Rust Programming Language]]

** Functional language features - Iterators and closures

[[https://doc.rust-lang.org/book/ch13-00-functional-features.html][Functional Language Features: Iterators and Closures]]

* Case Study 1 - Minigrep - demo app for learning purposes

[[https://doc.rust-lang.org/book/ch12-01-accepting-command-line-arguments.html][Accepting Command Line Arguments - The Rust Programming Language]]

* Final chapters

[[https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html][Unsafe Rust]]
[[https://doc.rust-lang.org/book/ch19-03-advanced-traits.html][Advanced Traits]]