Rust

Functions

Rust is an expression based language.

  • Statements are instructions that perform some action but do not return a value.

e.g. let x = 6;

  • Expressions evaluate to a resulting value.

e.g. x + 1

Crates

They are collection of rust code files. They are binary crates and library crates. Library crates contains code to be used in other programs an not on its own.

Doc

If you don't remeber which traits and functions are available in your codebase you can run

cargo doc --open

Shadowing

This is convenient when you want to reuse a variable without having to come up with a new name.

let x = 5;
let x = x + 1;
let x = x * 2;

Questions

What exactly is a variant. How is that different from a trait ?

Define

  • type
  • trait
  • struct
  • enum
  • variant
  • module
  • crate
  • arms: an arm consist of a pattern to be matched agaisnt and the code to be run if the pattern is matched.

For example

let guess: u32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => continue,
};

This is also the case in an if statement. Each blocks of code associated to the if and else are called arms.

fn main() {
    let number = 3;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

Iif Expressions

In rust, if should be associated to a boolean.

When you have more than one else if expression you might want to refactor your code. Rust has a powerful branching construct called match for these cases.

Data types

Two types of data types in Rust:

  • scalar
  • compound

Scalar

Represents a single value. Rust has four primary scalar types: integers, floating-point numbers, Booleans, and characters.

integers

Signed or unsigned. Signed can be negative or positive. Unsigned can only be positive.

Char

specified with single quote

e.g let c = 'z'; unlike strings which are specified with double quotes.

Compound

  • tuples cannot grow or shrink. fixed lenght. e.g. let tup: (i32, f64, u8) = (500, 6.4, 1);

To access values of the tuple we destructure it

let tup = (500, 6.4, 1);
let (x, y, z) = tup;

println!("The value of y is: {}", y);

Another way to acess elements of the tupple is by using the index

let x: (i32, f64, u8) = (500, 6.4, 1);

let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
  • arrays

Unlike tupples, every element must be of the same type. They are fixed lenght.

let a = [1, 2, 3, 4, 5];

To acess elements of an array we use the index

let a = [1, 2, 3, 4, 5];

let first = a[0];
let second = a[1];

The Slice type

Vectors

These are similar to arrays but can grow or shrink in size. They are stored in the heap rather than the stack which is the case for arrays.

Vectors can only store data of the same type.

The vec! macro is used to create a vector.

let v = vec![1, 2, 3];

Ownership

Ownership is a set of rule governing how Rust manages memory.

Three possibilities for programming languanges:

  • their is garbage collection periodically
  • the programmer must explicitily allocate and free memory
  • memory is managed through a system of ownership with a set of rules checked by the compiler (rust)

Stck versus heap

Think of stack as a pile of plates. last in, first out. You dont remove stuff from in between. Yopu push onto the stack or pop off the stack. All data on the stack must have known, fixed size.

On the hep, its different and less organized. The memory allocator check for a big enough spacem marks it as beeing in use and returns a pointer. This is called allocating. Because the pointer is know, fixed sized, you can in turn store it on the stack.

Pushing on the stack is much quicker because you just have to add stuff on top. No time wasted in finding free space. Similarly acccessing data on the stack is quicker.

Ownership rules

  • each values in Rust as an owner.
  • There can be only one owner at a time.
  • When the owner goes out of scope, the value will be dropped.

Variable scope

The scope is the range within the programm for which an item is valid.

    {                      // s is not valid here, it’s not yet declared
        let s = "hello";   // s is valid from this point forward

        // do stuff with s
    }                      // this scope is now over, and s is no longer valid

What happens behind the scenes is that rust's drop function is automatically runned at the closing opf the curly brackets.

References and borrowing

A reference is like a pointer in the sense that it is an adress to be followed to acess the data stored at this adress (data owned by another variable). But unlike a pointer, a reference is guaranteed to point to a valid value of a particular type for the life of that reference.

The ampersands represent a reference e.g &s1. Here you refer to some value, without taking ownership.

*is the dereferencing operator.

Because we nevere had ownership when using a reference, we do not need to return the values from a function to give back ownership. The creation of a reference is called borrowing. Like in real life, if a person owns something, you can, sometimes, borrow it. When you are done you give it back, because you dont own it.

You cannot modify stuff you borrow if it is not mutable. However you can make mutable references. One big restriction: if you have a mutable reference to a value, you cannot meke another reference to that value.

This allows rust to prevent data race condition.

This happens in the three sceanrii:

  • two or more pointers access the same data at the same time.
  • at least one of the pointers is beeing used to write to the data
  • no mechanism used to synchronize acess to the data.

Rules of references.

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

Structs

Structs are similar to Tuples. Like tuples pieces of a struct can be different. Unlioke tuples, in a Struct youll name each piece of data (these are called fields)

Once created, structs can be instantiated.

Tuple structs

e.g.

struct Color(i32, i32, i32);

Unit-like structs

Even simpler: they are things such as Unit-like structs.

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

Method syntax

Methods are like functions but unlike functions they are defined within the context of a struct (or an enum or a trait object). There first parameter is always self, which respresnets the instance of the struct the method is beeing called on.

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
    fn width(&self) -> bool {
        self.width > 0
    }
    fn can_hold(&self, other: &Rectangle) -> bool {
       self.width > other.width && self.height > other.height
    }
}

These methods are called associated functions, they are indeed associated to the struct Rectangle. We can define associated function that DO NOT have self as their first parameters (they are thus not methods.9 because they do not need an instance of the type to work with.

These associated functions which are not methods are often used for constructors that will return a new instance of the structs.

Enums

Enums allow to define a type by enumerating its possible variants. Option is a particularly usefull enum. Encodes that a value can be either something or nothing. Pattern matching using match makes it easy to run different code for different values of an enum.

One very common enum in rust is Option

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

You can even directly

let some_number = Some(5);
let absent_number: Option<i32> = None;

Option has a large number of associated methods that can be checked at https://doc.rust-lang.org/std/option/enum.Option.html

match control flow construct

You can thing of match as a coin sorting machine. The coins slide down a ramp with holes of incresaing size. The first hole fitting size sorts the condition. think of it as conditional expression with if. But the big difference is that it doesnt need to be a boolean.

Remeber that the match arms are made by two parts :

  • a pattern
  • some code

separtaed by =>

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

Combining mathc and enums is extremely frequent pattern in rust programming.

fn plus_one(x: Option) -> Option { match x { None => None, Some(i) => Some(i + 1), } }

fn main() { let five = Some(5); let six = plus_one(five); let none = plus_one(None);

println!("six is {:#?} and none is {:#?}", six, none);

}

Matches in Rust are exhaustive: all the cases must be covered.

catch all patterns and the _ placeholder

This is usefull when you which to aplly and action to some patterns but then apply the same action for the rest of the cases (i.e. a default action)

let config_max = Some(3u8);
match config_max {
    Some(max) => println!("The maximum is configured to be {max}"),
    _ => (),
}

The last arms catches all other possible cases. We are exhaustive and covered. The empty tuples means no action is taken.

if let (when match get's a bit wordy ...)

let config_max = Some(3u8);
if let Some(max) = config_max {
    println!("The maximum is configured to be {max}");
}

Pattern syntax

Patterns can be used to destructure struct, enums and tuples and use differents parts of these values.

fn main() {
    let p = Point { x: 0, y: 7 };

    match p {
        Point { x, y: 0 } => println!("On the x axis at {x}"),
        Point { x: 0, y } => println!("On the y axis at {y}"),
        Point { x, y } => {
            println!("On neither axis: ({x}, {y})");
        }
    }
}

Destructuring can work on nested object (here enums)

enum Color {
    Rgb(i32, i32, i32),
    Hsv(i32, i32, i32),
}

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(Color),
}

fn main() {
    let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));

    match msg {
        Message::ChangeColor(Color::Rgb(r, g, b)) => {
            println!("Change color to red {r}, green {g}, and blue {b}");
        }
        Message::ChangeColor(Color::Hsv(h, s, v)) => {
            println!("Change color to hue {h}, saturation {s}, value {v}")
        }
        _ => (),
    }
}

If you are not going to use a variable yet but want to ignore it. you prefix by _

fn main() { let _x = 5; let y = 10; }

Extra conditionals with Match guards.

A match guards is and additional if condition specified after the pattern in a match arms.Usefull to express complex logic when patterns are not enough.

Strings

String is actually implemented as a wrapper around a vector of bytes with some extra guarantees, restrictions and capabilities.

Traits

Shared functionalities across data types. Similar to "interfaces" in other languages

Modules

Hash Maps

these are equivalent to Python's dictionnaries, also called associative arrays in other languages.

HashMap<K, V> Stored on the heap.

Like Vectors, HashMaps are homegeneous, all of the keys miust have the same

The HashMaps takes owner ship of the values insert inside. You put them there by hashmap.insert(k, v)

Overwritting a value

if we hashmap.insert(k, v1) hashmap.insert(k, v2)

v1 is overwritten

Adding value only if absent

hashmap.insert(k, v1)

hashmap.entry(k1).or_insert(v2) hashmap.entry(k).or_insert(v2)

v1 is not overwritten because its already present