Skip to content

Instantly share code, notes, and snippets.

@nick-kravchenko
Last active September 13, 2024 12:11
Show Gist options
  • Save nick-kravchenko/ab9cd51c8b10081bafb60e500488235a to your computer and use it in GitHub Desktop.
Save nick-kravchenko/ab9cd51c8b10081bafb60e500488235a to your computer and use it in GitHub Desktop.
Rust

RUST

TOC

Fundamentals | Data types

Data Types

  • Memory only stores binary data
    • Anything can be represented in binary
  • Program determines what the binary represents
  • Basic types that are universally useful are provided by the language

Basic Data Types

  • Boolean
    • true, false
  • Integer
    • 1, 2, 3, 50, 99, -2
  • Double / Float
    • 1.1, 505, 200.0001, 2.0
  • Character
    • 'A', 'B', 'c', '6', '$'
  • String
    • "Hello", "string", "this is a string", "it's 42"

Recap

  • Anything can be represented with binary
  • Basic data types are:
    • boolean, integer, double/float, character, string

Fundamentals | Variables

What is a variable?

  • Assign data to a temporary memory location
    • Allows programmer to easily work with memory
  • Can be set to any value & type
  • Immutable by default, but can be mutable (!!!)
    • Immutable: cannot be changed (~ const)
    • Mutable: can be changed

Examples

let two = 2;
let hello = "hello";
let j = 'j';
let my_half = 0.5;
let mut my_name = "Bill";
let quit_program = false;
let your_half = my_half;

Recap

  • Variables make it easier to work with data
  • Variables can be assigned to any value
    • This include other variables
  • Immutable by default (!!!)

Fundamentals | Functions

What are functions

  • A way to encapsulate program functionality
  • Optionally accept data
  • Optionally return data
  • Utilized for code organization
    • Also makes code easier to read

Anatomy of a function

// fn - keyword to declare a function
// add - name of the function
// (a: i32, b: i32) - function parameters
// a, b - variables used inside a function
// i32 - 32bit integer type
// -> i32 - return type

fn add(a: i32, b: i32) -> i32 {
  a + b
}

Using a function

fn add(a: i32, b: i32) -> i32 {
  a + b
}

let x = add(1, 1); // 2
let y = add(3, 0); // 3
let z = add(x, 1); // 3

Recap

  • Functions encapsulate functionality
  • Useful to organize code
  • Can be executed by "calling" the function
  • Parameters determine what data a function can work with
  • Optionally "returns" data
    • Data sent back from the function

Fundamentals | Println macro

The println macro

  • Macros expand into additional code
  • println! "Prints" (displays) information to the terminal
  • Useful for debugging
    let life = 42;
    println!("hello"); // hello
    println!("{:?}", life); // 42
    println!("{:?} {:?}", life, life); // 42 42
    println!("the meaning is {:?}", life); // the meaning is 42
    println!("the meaning is {life:?}"); // the meaning is 42
    println!("{life}"); // 42 but visible for end user

Recap

  • Macros use an exclamation point to call/invoke
  • Generate additional Rust code
  • Data can be printed using println!:
    • {:?}
    • {varname:?}
    • {varname}

Fundamentals | Control flow using if

Execution Flow

  • Code executed line-by-line
  • Actions are performed & control flow may change
    • Specific conditions can change control flow
      • if
      • else
      • else if

Example - if..else

let a = 99;
if a > 99 {
  println!("Big number");
} else {
  println!("Small number");
}

Example - if..else if..else

let a = 99;
if a > 200 {
  println!("Huge number");
} else if a > 99 {
  println!("Big number");
} else {
  println!("Small number");
}

// This will not work
if a > 99 {
  println!("Big number");
} else if a > 200 {
  println!("Huge number");
} else {
  println!("Small number");
}

Recap

  • Code executes line-by-line
    • This can be changed using if
  • Try to always include else, unless there truly is no alternative case

Fundamentals | Repetition using loops

Repetition

  • Called "looping" or "iteration"
  • Multiple types of loops
    • loop - infinite loop
    • while - conditional loop

Loop

let mut a = 0;
loop {
  if a == 5 {
    break;
  }
  println!("{:?}", a);
  a = a + 1;
}
// > 0
// > 1
// > 2
// > 3
// > 4

While loop

let mut a = 0;
while a != 5 {
  println!("{:?}", a);
  a = a + 1;
}
// > 0
// > 1
// > 2
// > 3
// > 4

Recap

  • Repetition can be perormed using loops
    • While loop
    • Infinite loop
  • Both types of loops can exit using break

Demo | Basic arithmetic

fn sub(a: i32, b: i32) -> i32 {
  a - b
}

fn main() {
  let sum = 2 + 2;
  let value = 10 - 5;
  let division = 10 / 2;
  let mult = 5 * 5;

  let five = sub(8, 3);

  let rem = 6 % 3; // 0
  let rem2 = 6 % 4; // 2
}

Fundamentals | Match

Match

  • Add logic to program
  • Similar to if..else
  • Exhaustive
    • All options must be accounted for

Example

fn main() {
  let some_bool = true;
  match some_bool {
    true => println!("its true"),
    false => println!("its false"),
  }
}

Example with int

fn main() {
  let some_int = 3;
  match some_int {
    1 => println!("its 1"),
    2 => println!("its 2"),
    3 => println!("its 3"),
    _ => println!("its something else"), // everything else (~ default from switch/case)
  }
}

match vs else..if

  • match will be checked by the compiler
    • If a new possibility is added, you will be notified when this occurs
  • else..if is not checked by the compiler
    • If a new possibility is added, your code may contain a bug

Recap

  • Prefer match over else..if when working with a single variable
  • match considers all possibilities
    • More robust code
  • User underscore (_) to match "anything else"

Working With Data | enum

Enumeration

  • Data that can be one of multiple different possibilities
    • Each possibility is called a "variant"
  • Provides information about your program to the compiler
    • More robust programs

Example

enum Direction {
  Up,
  Down,
  Left,
  Right,
}

fn with_way(go: Direction) {
  match go {
    Direction::Up => "up",
    Direction::Down => "down",
    Direction::Left => "left",
    Direction::Right => "right",
  }
}

Recap

  • Enums can only be one variant at a time
  • More robust programs when paired with match
  • Make program code easier to read

Activity | enum

enum Color {
  Red,
  Yellow,
  Blue,
}

fn print_color (my_color: Color) {
  match my_color {
    Color::Red => println!("red"),
    Color::Yellow => println!("yellow"),
    Color::Blue => println!("blue"),
  }
}

fn main() {
  print_color(Color::Blue);
}

Working With Data | struct

Structure

  • A type that contains multiple pieces of data
    • All or nothing - cannot have some pieces of data and not others
  • Each piece of data is called a "field"
  • Makes working with data easier
    • Similar data can be grouped together

Example

struct ShippingBox {
  depth: i32,
  width: i32,
  height: i32,
}
let my_bos = ShippingBox {
  depth: 3,
  width: 2,
  height: 5,
}
let tall = my_box.height;
pringln!("the box is {:?} units tall", tall);

Recap

  • Structs deal with multiple pieces of data
  • All fields must be present to create a struct
  • Fields can be accessed using dot(.)

Activity | struct

enum Flavour {
  Sparkling,
  Sweet,
  Fruity,
}

struct Dring {
  flavour: Flavour,
  fluid_oz: f64,
}

fn print_drink(drink: Drink) {
  match drink.flavour {
    Flavour.Sparkling => println!("flavour: sparkling"),
    Flavour.Sweet => println!("flavour: sweety"),
    Flavour.Fruity => println!("flavour: fruity"),
  }
  println!("oz: {:?}", drink.fluid_oz);
}

fn main() {
  let sweet = Drink {
    flavour: Flavour::Sweet,
    fluid_oz: 6.0,
  };
  print_drink(sweet);
  let fruity = Drink {
    flavour: Flavour::Fruity,
    fluid_oz: 10.0,
  };
  print_drink(fruity);
}
// > flavour: sweety
// > oz: 6.0
// > flavour: fruity
// > oz: 10.0

Working With Data | Tuples

Tuples

  • A type of "record"
  • Store data anonymously
    • No need to name fields
  • Useful to return pairs of data from functions
  • Can be "destructured" easily into variables

Example

enum Access {
  Full,
}

fn one_two_three() -> (i32, i32, i32) {
  (1, 2, 3)
}

let numbers = one_two_three();
let (x, y, z) = one_two_three();
println!("{:?}, {:?}", x, numbers.0); // 1 1
println!("{:?}, {:?}", y, numbers.1); // 2 2
println!("{:?}, {:?}", z, numbers.2); // 3 3

let (employee, access) = ("Jake", Access::Full);

Recap

  • Allow for anonymous data access
  • Useful when destructuring
  • Can contain any number of fields
    • Use struct when more than 2 or 3 fields

Fundamentals | Expressions

Expressions

  • Rust is an expression-based language
    • Most things are evaluated and return some value
  • Expression values coalesce to a single point
    • Can be used for nesting logic

Example if

let my_num = 3;
let is_lt_5 = if my_num < 5 {
  true
} else {
  false
};

let is_lt_5 = my_num < 5;

Example match

let my_num = 3;
let message = match my_num {
  1 => "hello",
  _ => "goodbye",
};

Example enum + match

enum Menu {
  Burger,
  Fries,
  Drink,
}

let paid = true;
let item = Menu::Drink;
let drink_type = "water";
let order_placed = match item {
  Menu::Drink => {
    if drink_type == "water" {
      true
    } else {
      false
    }
  },
  _ => true,
};

Recap

  • Expressions allow nested logic
  • if and match expressions can be nested
    • Best to not use more than two or three levels

Fundamentals | Intermediate Memory

Basic memory refresh

  • Memory is stored using binary
    • Bits: 0 or 1
  • Computer optimized for bytes
    • 1 byte == 8 contiguous bits
  • Fully contiguous

Addresses

  • All data in memory has an "address"
    • Used to locate data
    • Alwayes the same - only data changes
  • Usually don't utilize addresses directly
    • Variables handle most of the work

Offsets

  • Items can be located at an address using an "offset"
  • Offsets begin at 0
  • Represent the number of bytes away from the original address
    • Normaly deal with indexes instead

Addresses & Offsets

     Offset
   _____⊥______
  /            \
#   0  1  2  3
0  [ ][ ][ ][ ]
4  [ ][*][ ][ ]
8  [ ][ ][ ][ ]
12 [ ][ ][ ][ ]
16 [ ][ ][ ][#]

0, 4, 8, 12, 16 - Address
[ ] - 1 byte
[*] - Address 4, offset 1 | Data[1]
[#] - Data[3]

Recap

  • Memory uses addresses & offsets
  • Addresses are mermanent, data differs
  • Offsets can be used to "index" into some data

Fundamentals | Ownership

Managing memory

  • Programs must track memory
    • If they fail to do so, a "leak" occurs
  • Rust utilizes an "ownership" model to manage memory
    • The "owner" of memory is responsible for cleaning up the memory
  • Memory can either be "moved" or "borrowed"

Example - Move

enum Light {
  Bright,
  Dull,
}

fn display_light(light: Light) {
  match light {
    Light::Bright => println!("bright"),
    Light::Dull => println!("dull"),
  }
}

fn main() {
  let dull = Light::Dull;
  /**
    * moving into the function.
    * display_light owns variable dull
    * and requires to delete data once the function completes
    */
  display_light(dull); 
  /** ❌❌❌
    * dull variable is deleted and unaccessable.
    * cant use it no more.
    */
  display_light(dull);
}

Example - Borrow

enum Light {
  Bright,
  Dull,
}

fn display_light(light: &Light) {
  match light {
    Light::Bright => println!("bright"),
    Light::Dull => println!("dull"),
  }
}

fn main() {
  let dull = Light::Dull;
  /**
    * letting function to BURROW dull variable using &
    * main function is still the owner of the dull variable
    */
  display_light(&dull);
  /** ✅✅✅
    * dall variable still exists
    * can use it again
    */
  display_light(&dull);
}

Recap

  • Memory must be managed in some way to prevent leaks
  • Rust uses "ownership" to accomplish memory management
    • The "owner" of data must clean up the memory
    • This occurs automatically at the end of the scope
  • Default behavior is to "move" memory to a new owner
    • Use an ampersand (&) to allow code to "borrow" memory

Demo | impl

// ~ Interface
struct Temperature {
  degrees_f: f64,
}

// ~ Class
impl Temperature {
  fn freezing() -> Self { // can also use -> Temperature
    Self { degrees_f: 32.0 }
  }

  fn show_temp(&self) {
    println!("{:?} degrees F", self.degrees_f);
  }
}

fn main() {
  let hot = Temperature { degrees_f: 99.9 };
  hot.show_temp(); // ~ using as an instantiated entity method
  let freezing = Temperature::freezing(); // ~ using as a static method
  freezing.show_temp();
}

Data Structures | Vector

Vector

  • Multiple pieces of data
    • Must be the same type
  • Used for lists of information
  • Can add, remove and traverse the entries

Example declaration

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

let mut my_numbers = Vec::new();
my_numbers.push(1);
my_numbers.push(2);
my_numbers.push(3);
my_numbers.pop();
my_numbers.len(); // this is 2

let two = my_numbers[1];

Example iteration

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

for num in my_numbers {
  println!("{:?}", num);
}
// > 1
// > 2
// > 3

Recap

  • Vectors contain multiple pieces of similar data
  • Data can be added or removed
  • The vec! macro can be used to make vectors
  • Use for..in to iterate throug items of a vector

Demo | Vector

struct Test {
  score: i32,
}

fn main() {
  let my_scores = vec![
    Test { score: 90 },
    Test { score: 88 },
    Test { score: 77 },
    Test { score: 93 },
  ];

  for test in my_scores {
    println!("score = {:?}", test.score);
  }
}
// > score = 90
// > score = 88
// > score = 77
// > score = 93

Data Types | String

String and &str

  • Two commonly used types of strings
    • String - owned
    • &str - borrowed String slice
  • Must use an owned String to store in a struct
  • Use &str when passing to a function

Example - Pass to function

fn print_it(data: &str) {
  println!("{:?}", data);
}

fn main() {
  print_it("a string slice");
  let owned string = "owned string".to_owned();
  let another_owned = String::from("another");
  print_it(&owned_string);
  print_it(&another_owned);
}

Example - Will not work

struct Employee {
  name: &str, // ❌❌❌
}

fn main() {
  let emp_name = "Jayson"; // creating a borrowed string
  let emp = Employee {
    name: emp_name,
  };
}

Example - Works!

struct Employee {
  name: String, // ✅✅✅
}

fn main() {
  let emp_name = "Jayson".to_owned();
  // OR let emp_name = String::from("Jayson");
  let emp = Employee {
    name: emp_name,
  };
}

Recap

  • String are automatically borrowed
  • Use .to_owned() or String::from() to create an owned copy of a string slice
  • Use an owned String when storing in a struct

Demo | Derive

/**
  * derive -
  * special macro that applied to enums and structs
  * and allows you to automatically implement some sort of functionality
  * in this case we're going to implement printing
  *
  * Clone, Copy - informs to compiler that allows it automatically make a copy when storing it to a struct or a function
  * ownership is no longer transferred when you move enumeration into structure or a function and a cope made instead
  */
#[derive(Debug, Clone, Copy)]
enum Position {
  Manager,
  Supervisor,
  Worker,
}

#[derive(Debug, Clone, Copy)]
struct Employee {
  position: Position,
  work_hours: i64,
}

fn main() {
  let me = Employee {
    position: Position::Worker,
    mork_hours: 40,
  };
  match me.position {
    Position::Manager => println!("manager"),
    Position::Supervisor => println!("supervisor"),
    Position::Worker => println!("worker"),
  }
  // > worker

  // with #[derive(Debug)]
  println!(":?", me.position);
  // > Worker

  // with #[derive(Debug)]
  println!(":?", me);
  // > Employee { position: Worker, work_hours: 40 }
}

Fundamentals | Type Annotations

Type Annotations

  • Required for function signatures
  • Types are usually inferred
  • Can also be specified in code
    • Explicit type annotations

Example - Basic

fn print_many(msg: &str, count: i32) { }

enum Mouse {
  LeftClick,
  RightClick,
  MiddleClickm
}

let enum: i32 = 15;
let a: char = 'a';
let left_click: Mouse = Mouse::LeftClick;

Example - Generics

enum Mouse {
  LeftClick,
  RightClick,
  MiddleClickm
}

let numbers: Vec<i32> = vec![1, 2, 3];
let letters: Vec<char> = vec!['a', 'b'];
let clicks: Vec<Mouse> = vec![
  Mouse::LeftClick,
  Mouse::RightClick,
  Mouse::MiddleClick,
];

Recap

  • Type annotations are mostly optional within function bodies
    • Occasionally required if compiler infer the type
  • Can be specified when using let bindings

Working With Data | enum revisited

Enums

  • enum is a type that can represent one item at a time
    • Each item is called a variant
  • enum is not limited to just plain variants
    • Each variant can optionally contain additional data

Example 1

enum Mouse {
  LeftClick,
  RightClick,
  MiddleClick,
  Scroll(i32),
  Move(i32, i32),
}

Example 2

enum PromoDiscount {
  NewUser,
  Holiday(String),
}

enum Discount {
  Percent(f64),
  Flat(i32),
  Promo(PromoDiscount),
  Custom(String),
}

Recap

  • enum variants can optionally contain data
    • The data can be another enum
  • Can mix plain identifiers and data-containing variants within the same enum
  • More than one piece of data can be associated with variant

Demo | Advanced match

enum Discount {
  Percent(i32),
  Flat(i32),
}

struct Ticket {
  event: String,
  price: i32,
}

fn main() {
  let n = 3;
  match n {
    3 => println!("three");
    other => println!("number: {:?}", other);
  }

  let flat = Discount::Flat(2);
  match flat {
    Discount::Flat(2) => println!("flat 2"),
    Discount::Flat(amount) => println!("flat discount of {:?}", amount),
    _ => (),
  }

  let concert = Ticket {
    event: "concert".to_owned(),
    price: 50.0,
  };
  match concert {
    Ticket {price: 50, event} => println!("event @ 50 = {:?}", event),
    Ticket {price, ..} => println!("price = {:?}", price),
  }

  // > three
  // > flat 2
  // > event @ 50 = "concert"
}

Working With Data | Option

Option

  • Option type that may be one of two things
    • Some data of a specified type
    • Nothing
  • Used in scenarious where data may not be required or is unavailable
    • Unable to find something
    • Ran out of items in a list
    • Form field not filled out

Definition

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

Example 1

struct Customer {
  age: Option<i32>,
  email: String,
}

let mark = Customer {
  age: Some(22),
  email: "[email protected]".to_owned(),
};
let becky = Customer {
  age: None,
  email: "[email protected]".to_owned(),
};
match becky.age {
  Some(age) => println!("customer is {:?} years old", age),
  None => println!("customer age not provided"),
}

Example 2 (Function returns None)

struct GroceryItem {
  name: string,
  qty: i32,
}

fn find_quantity(name: &str) -> Option<i32> {
  let groceries = vec![
    GroceryItem { name: "bananas".to_owned(), qty: 4, },
    GroceryItem { name: "eggs".to_owned(), qty: 12, },
    GroceryItem { name: "bread".to_owned(), qty: 1, },
  ];
  for item in groceries {
    if item.name == name {
      return Some(item.qty);
    }
  }
  None
}

Recap

  • Option represents either some data or nothing
    • Some(variable_name)
      • Data is available
    • None
      • No data is available
  • Useful when needing to work with optional data
  • Use Option<type> to declare an optional type

Working With Data | Result

Result

  • A data type that contains one of two typed of data:
    • "Successful" data
    • "Error" data
  • Used in scenarious where an action needs to be taken, but has the possibility of failure
    • Copying a file
    • Connection to a website

Definition

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

Example

fn get_sound(name: &string) -> Result<SoundData, String> {
  if name == "alert" {
    Ok(SoundData::new("alert"))
  } else {
    Err("unable to find sound data".to_owned())
  }
}

let sound = get_sound("alert");
match sound {
  Ok(_) => println!("sound data located"),
  Err(e) => println!("error: {:?}", e),
}

Recap

  • Result represents either success or failure
    • Ok(variable_name)
      • The operation was completed
    • Err(variable_name)
      • The operation failed
  • Useful when working with functionaluty that can potentialy fail
  • Use Result<T, E> when working with results

Data Structures | Hashmap

Hashmap

  • Collection that stores data as key-value pairs
    • Data is located using the "key"
    • The data is the "value"
  • Similar to definitions in a dictionary
  • Very fast to retreive data using key

Example: find data

let mut people = HashMap::new();
poeople.insert("Susan", 21);
poeople.insert("Ed", 13);
poeople.insert("Will", 14);
poeople.insert("Cathy", 22);
poeople.remove("Susan");

match people.get("Ed") {
  Some(age) => println!("age = {:?}", age),
  None => println!("not found"),
}

Example: iterate

for (person, age) in people.iter() {
  println!("person = {:?}, age = {:?}", person, age);
}

for person in people.keys() {
  println!("person = {:?}", person);
}

for age in people.values() {
  println!("age = {:?}", age);
}

Recap

  • Store information as key-value pairs
    • "Key" is used to access the "value"
  • Very fast to insert & find data using key
  • Useful when you need to find information and know exactly where it is (via the key)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment