Anton Shuvalov

Notes on Rust

Variables & Scope

Immutable variables are declared with let or const operators and block-scoped. To declare a mutable variable use mut modifier, (e.g. let mut x = 5;).

fn main() {
  let x = 1;
  {
    // there is no `x` variable.
    let x = 2;
  }
}

Destructuring

Destructuring declaration is supported:

let (a, b) = (1, 2);

Functions

Functions in Rust declared via fn keyword.

fn do_stuff(qty: f64, oz: f64) -> f64 {
  return qty * oz;
}

Return statement may be omitted, so { return true } is equal to { true }.


Imports and Dependencies

Public functions declared with pub can be used either by cargo_package_name::pub_fn_name(), or just by their names if they were imported with use cargo_package_name::pub_fn_name statement.

Dependencies should be specified in a Cargo.toml file under [dependencies] section in a format like: package = "version"


Control Flow

if keyword:

msg = if num == 5 { "five" } else { "other" }

Loops are declared with loop keyword.

loop { break; }

Labeled loops are declared with 'label: statement:

'bob: loop {
    loop {
    break 'bob;
    continue 'bob;
    }
}

while loop:

while do_stuff() {}

// is equal to:
loop {
    if !do_stuff() { break; }
}

for loop:

for (x,y) in [(1,1), (2,2), (3,3)].iter() {}

Ranges:

for num in 0..50 {} // exclusive range (till 49)
for num in 0..=50 {} // inclusive range (till 50)

Ownership & References

Ownership

Rust adopts an explicit ownership concept. Each value the only one owner. If owner is removed, value is also removed.

let s1 = String::from("abc");
let s2 = s1; // s1 value is moved and owned by s2
println!("{}", s1); // Error! s1 is uninitialized

To make a copy there is a s1.clone() method that clones a value.

let s1 =  String::from("abc")
do_stuff(s1); // moved ownership to the do_stuff local variable
println!(s1); // Error, moved!

There is a reference concept to address this problems.

References

References may be defined by & for immutable reference and &mut for mutable reference. De-referencing to the value is done automatically, but there is also a manual way with the *ref operator.

Rust has a special thread safe rule, that in any given time, you may have either only one mutable reference or any number of immutable references across all threads.

let s1 = String::from("abc");

// Immutable reference
do_stuff(&s1);
fn do_stuff(s: &String) {}

// Mutable reference
do_stuff(&mut S1);
fn do_stuff(s: &mut String) {}

Structs & Traits

Rust takes composition-over-inheritance approach, so it has neither no classes nor inheritance. To design in rust you should make use of structs describes a data structure and traits describes an interface.

Struct

struct may have data fields, methods and associated functions. An example struct:

struct RedFox {
  enemy: bool,
  life: u8,
}

// Instantiating
let fox = RedFox {
  enemy: true,
  life: 70,
}

impl block contains associated functions (methods):

// or by implementing a new() method provides default values:
impl RedFox {
  fn new() -> Self {
    Self {
      enemy: true,
      life: 70,
    }
  }
  some_method(self) ...
  other_method(&self) ...
}

let fox = RedFox::new();

Traits

Traits are the way to provide generic interfaces. Rust compiles code for each generic data.

struct RedFox { ... }

trait Noisy {
  fn get_noise(&self) -> &str;
}

impl Noisy for RedFox {
  fn get_noise(&self) -> &str { "Meow?" }
}
fn print_noise<T: Noisy>(item: T) {
  println!("{}", item.get_noise());
}

Trait may have default implementation:

trait Run {
  // add a trait with a default implementation
  fn run(&self) {
    println!("I'm running!");
  }
}

struct Robot {}

// Use default implementation
impl Run for Robot {};

Traits also used for interfacing things like Copy, if a structure implements Copy trait, it will be copied rather than moved.


Pattern Matching

if let Some(x) = my_variable {
    ... do smth with this variable
}
match my_variable {
    Some(x) => {
        // ...
    }
    Some(x) if x < 3.0 => 2;
    None => {
        // ...
    }
    _ => {
        // match anything
    }
}

Guards

Guards is another way to evaluate an expression depends on a given condition

let shot = match coord.distance_from_center() {
   x if x < 1.0 => Shot::Bullseye,
   x if x < 5.0 => Shot::Hit(x),
   _ => Shot::Miss,
};

Closures

Closure is the same thing like Python lambdas or arrow functions in JS. It's an anonymous function can borrow or capture data from the scope it nested to.

|x, y| { x + y}

let s = "abc".to_string();

// borrow ref to s into a closure
let f = || {
    println!("{}", s);
}

// move variables into a closure
let f = move || {
    ...
}

Closures are broadly used for functional programming in Rust.

let mut v = vec![2, 4, 6];

v.iter()
    .map(|x| x* 3)
    .filter(|x| *x > 10*)
    .fold(0, |acc, x| acc + x);

Threads

use std::thread;

fn main() {
    let handle = thread::spawn(move || {
        // do stuff
    });

    // continue main thread

    // wait until thread has exited
    handle.join().unwrap();
}

Scalar Types

  • Booleans: bool. Either true or false.
  • Integers: i8, i16, i32, i64, i128, u8, u16, u32, u64, u128. Default: i32.
  • Floats: f32 f64. Default: f64.
  • inline types:
    • Decimal: 1000000
    • Hex: 0xdeadbeef
    • Octal: 0o77543211
    • Binary: 0b11100110
    • Byte: b'A'
  • characters: char always 32 bits

Numbers can be separated with _ symbol, that will be omitted on interpretation (1_000_000 is 1000000, etc)

Types also may be specified within suffixes ( 5u16, 3.14f32, etc).


Compound Types

  • Tuples: let info: (u8, f64, i32) = (1, 3.3, 999), can be accessed by info.0 or with destructuring let (a, b, c) = info;
  • Array: let arr: [u8; 3] = [1, 2, 3] up to 32 element

Strings

  • string is an immutable string
  • String is a mutable vector with a capacity

Enums

Enum is a data type consisting of a set of elements, and the value matches to any one of its elements.

Primitive Enum:

enum Color { Red, Green, Blue }
let color = Color::Red;

Data Mapping:

enum Something<T> {
    Empty,
    Some(T),
    Ammo(u8),
    Things(String, i32),
    Place {x: i32, y: i32}
}

Option & Result

Both Option and Result enums are pretty used in standard library.

Option represents an enum, that may be either type T or None.

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

An example of how to deal with Option:

let mut x: Option<i32> = None;
x = Some(5);
x.is_some(); // true
x.is_none(); // false
for i in x {
    println!("{}", i); // prints 5
}

Result is an enum that may be either a type T or an error E.

#[must_use]
enum Result<T, E>  {
    Ok(T),
    Err(E),
}

An example of Result:

use std::fs::File;

fn main() {
    let res = File::open("foo");
    let f = res.unwrap(); // panic if result is an error
    let f = res.expect("error message"); // same but w/ an error message
    if res.is_ok() { ... }
    match res {
        Ok(f) => { ... }
        Err(e) => { ...}
    }
}

Collections

Vectors

Vector is a generic collection of one type behaves as a stack.

// Create a vector with a struct
let mut v: Vec<i32> = Vec::new();
v.push(2);
v.push(4);
v.push(6);


// Create a vector with a macros
let vec = vec![2, 4, 6];

HashMap

HasMap is a dictionary

HashMap<K, V>
let mut h: HashMap<u8, bool> = HashMap::new();
h.insert(5, true);
h.insert(6, false);
let have_file = h.remove(&5).unwrap();

VecDeque

Is a double-ended queue implemented with a ring-buffer


Further Reading:


Visualizing Compilation

Try -Ztimings flag to get visualization. For details see this link


Explanation of Errors

Explanation of errors: rustc --explain E0384


Further Reading:

Created with obsidian-blog