'Introduction to the Rust Programming Language' Workshop from Frontend Masters

What Can I Build with Rust?

  • web servers
  • CLIs
  • native desktop apps
  • in-browser apps via WebAssembly
  • performance-intensive libraries
  • operating systems

Why to use?

  • Speed, Performance, Going Real Fast
  • like an upgrade to C and C++
    • has better error messages
    • nice ergonomics and a language server
    • (mostly) automatic memory management
    • a package manager and code formatter
    • more compiler help with concurrency
    • lots of compiler help for big code bases
  • "most loved programming language" 2016-2020 on stackoverflow

Why not to use?

  • big language - lots to learn!
  • smaller ecosystem than C/C++ (but it has an FFI-Foreign Function Interface)
  • slower iteration cycle than most languages
    • strict compiler
    • satisfying ("fighting") the borrow checker
    • slow compile times for full builds
    • tests can take a while to build
  • safer than C++, but less safe than pure FP

Misc.

  • compiles to either machine code or web assembly
  • can't compile for different systems, with out compiling on that system (i.e. no compiling for windows from mac and vice versa)
  • no classes, prototypes, or inheritance
  • can create your own iterables and define how to iterate
  • in general, Rust prefers to give errors at compile time rather than runtime
  • doesn't have an equivalent of JS's null or undefined

PART 1 : PRIMITIVES

Strings

Hello, World!

  • fn main() {
      println!("Hello, World!");  // "Hello, World!"
    }
    • put this in a fie app.rs and compile it by rustc app.rs and it will generate a binary executable called app, which you can run to see the output

Defining Variables with let

  • all variables in rust are declared with let --- do not need to initialize it when it is declared, but must be initialized before it's used
fn main() {
    let greeting = "Hello";
    let subject = "World";

    println!("{}, {}!", greeting, subject); // "Hello, World!"
}
  • println! uses string interpolation -- replaces first {} with greeting and second {} with subject
    • string interpolation can't be used everywhere

format!

  • can also use string interpolation
  • returns the string after making any {} substitutions
fn main() {
    let subject = "World";
    let greeting = format!("Hello, {}!", subject);
}

Crashing with panic!

  • can also use string interpolation
  • used to intentionally crash a program (no try/catch in Rust so it will really crash) => nothing after the panic! will run
fn main() {
    let crash_reason = "Server wanted a nap.";

    panic!("I crashed! {}", crash_reason);

    println!("This will never get run.");
}

Floats

  • println! also works with numbers
    fn main() {
      let x = 1.1;
      let y = 2.2;
    
      println!("x times y is {}", x * y);
    }
  • dividing by 0, gives infinity (not panic)

Mutability

  • let works, by default, like JS's const -- immutable
  • can use let mut to make it mutable (less common)

Numeric Types

  • type errors given at compile time
  • statically type-checked language --- every value has a single type associated with it at compile time and that type can never change at runtime
  • use type annotations to specify exactly which types the values have
    • fn main() {
          let x: f64 = 1.1;
          let y: f64 = 2.2;
      
          println!("x times y is {}", x * y);
      }
      • says that x and y both have type f64 (i.e 64-bit floating-point number)
  • Rust has type inference --- if it is unable to infer the type it will ask you to annotate the type at compile time --- will never silently infer the wrong type

Functions

  • when writing functions must specify types of inputs and return --- not inferred
    • fn main() {
          println!("1.1 times 2.2 is {}", multiply_both(1.1, 2.2));
      }
      
      fn multiply_both(x: f64, y: f64) -> f64 {
          return x * y;
      }
      • main has no arguments and no return
      • multiply_both takes two f64 arguments and returns an f64 value
  • if it ends in ! it is a macro, not a function
    • macros can use string interpolation, functions can't
    • can't be passed around like functions --- Rust has first class functions
    • click here to read about macros

Sizes

  • f32 - 4 bits
  • f64 - 8 bits => more memory used => allows for more precision, but may also slow down the program

Integers

  • can use _ where a comma would be in a number --- i.e. 1000 can be written as 1_000
  • dividing by 0 causes a panic

Sizes

  • signed:
    • i8 - 8 bits (1B), -127 to 128
    • i16 - 16 bits (2B), -32,768 to 32,767
    • i32 - 32 bits (4B), ...
    • i64 - 64 bits (8B), ...
    • i128 - 128 bits (16B), ...
  • unsigned:
    • u8- 0 to 255
    • u16 - 0 to 65,535
    • u32 - 0 to 4,294,967,295
    • u64 - 0 to 18,446,744,073,709,551,615
    • u128 - 0 to 170,141,183,460,469,231,731,687,303,715,884,105,728
    • char - a u32 that's been Unicode validated
  • packages are available for arbitrary ints, but shouldn't really need anything bigger than i128 or u128

Converting Numbers with as

  • as can only be used on numeric types
  • if try to use different types interchangeably you get a type mismatch error at compile time
    fn main() {
        let x: f64 = 1.1;
        let y: f32 = 2.2;
    
        println!("x times y is {}", x * y); // ERROR: mismatched types!
    }
    fn main() {
        let x: f64 = 1.1;
        let y: f32 = 2.2;
    
        println!("x times y is {}", x * y as f64); // x times y is 2.4200000524520875
    }
    • f32 -> f64 takes more memory
    • f64 -> f32 can result in information loss
      • => f64 -> f32 -> f64 can result in a different value than you started out with
  • can't mix signed and unsigned integers
    fn multiply(x: i64, y: u8) -> i64 {
      return x * (y as i64);
    }
    
    fn divide(x: i32, y: u16) -> f64 {
      return x as f64 / y as f64;
    }

Booleans

  • can use as
    • true as u8 evaluates to 1
    • false as u8 evaluates to 0
  • 1 == 2 evaluates false
  • no truthiness in rust -- only strict true and false

Conditionals

  • syntax uses if/else/else if with {}
    if cats > 1 {
      println!("Multiple cats!");
    } else if cats > 1_000 {
      println!("Too many cats!");
    } else {
      println!("Need more cats!")
    }
  • if can be used like a ternary, no separate syntax
    let suffix = if same_name_as_parent_and_grandparent { 
      "III" 
    } else if same_name_as_parent {
      "Jr"
    } else if same_name_as_child {
      "Sr"
    } else {
      "!"
    };
    
    println!("My name is {}{}", name, suffix);
    • must contain an else as a catch all in case all previous conditions are false --- won't compile without it
  • can write on one line if short
    let result = if a > b { a } else { b };

Statements and Expressions

  • an expression evaluates to a value cats > 1000
  • a statement does NOT evaluate to a value println!("Sooo many cats!");
    • statements terminate with ; Returning an Expression
  • if last line in function is an expression, automatically returned (must follow statements --- if multiple expressions then the return is needed)
  • comparison:

Expressionexpression

Statementstatement

diffs: Differences

PART 2: COLLECTIONS

  • everything in this part is copy by value

Tuples

  • a collection of 2 or more values (except for unit, see next section) let point: (i64, i64, i64) = (0, 0, 0);
    • can have mixed types and more or less values
  • assign values from the tuple with name and index
    let x = point.0;
    let y = point.1;
    let z = point.2;
    or with destructuring
    let(x, y, z) = point;
    • can partially destructure by using underscores as placeholder for parts don't care about
      let (x, y, _) = point;
      let (x, _, _) = point;
      let (_, y, _) = point;
  • can use let mut to make a mutable tuple
    let mut point: (i64, i64, i64) = (0, 0, 0);
    
    point.0 = 17;
    point.1 = 42;
    point.2 = 90;
  • tuples can't change size at runtime

Unit

  • the zero tuple --- has nothing inside of it let unit: () = ();
  • used as a return for functions with no return value because every function is required to have a return value (no void in Rust) --- if nothing useful to return then by convention it should return () --- e.g. main and println! both return ()

Structs

  • syntax sugar for tuples --- identical at runtime (i.e. no performance tradeoffs) --- difference is that at compile time struct elements are referenced by name rather than by position
  • same as point in tuple example, but now the elements are named
    struct Point {
      x: i64,
      y: i64,
      z: i64,
    }
    • examples of how we could construct a new Point:
      fn new_point(x: i64, y: i64, z: i64) -> Point {
        Point { x: x, y: y, z: z }  // or with syntax sugar: Point { x, y, z }
      }
      or let point = Point { x: 1, y: 2, z: 3 };
  • notice we now use {} instead of () like we did with tuples
  • can also have mixed types
  • can have mutable structs let mut point = Point { x: 1, y: 2, z: 3 }; and can do things like point.x = 5;
  • getting values out (using second new point example from above)
    let x = point.x;                    // with struct name and element name
    let Point { x, y, z } = point;      // with destructuring
    let Point { x, y: _, z } = point;   // destructuring when don't care about y
    let Point { x, z, .. } = point;     // pretend all other fields are underscores
    let Point { x, .. } = point;
    • notice in second to last example, because they are named, the order doesn't matter
  • can't change number of elements or names of elements at runtime
  • possible to put functions in a struct, but not straight forward

Arrays

let mut years: [i32; 3] = [1995, 2000, 2005];

  • NO mixed types --- all elements in example must be of type i32
  • fixed-length --- example has fixed length of 3 elements
  • get values out
    let first_year = years[0];  // by index/name --- can use other things as long as not out of bounds because 
                                //                     it will panic if out of bounds
    let [_, second_year, third_year] = years;     // with destructuring
  • assigning
    years[2] = 2010;  // compile time check, never panic
    years[x] = 2010;  // runtime check (because it doesn't necessarily know what x will be) and can potentially panic
  • can iterate (tuples and structs NOT iterable -- over values or fields)
    for year in years.iter() {
      println!("Next year: {}", year + 1);
    }
    • allowed to iterate because always know what the type will be (tuples can have mixed types)

Memory

memory 1

memory 2

memory 3

memory 4

memory 5

memory 6

memory 7

memory 8

memory 9

memory 10

memory 11

memory 12

memory 13

Part 3: PATTERN MATCHING

Enums

  • type goes after enum, then assign variants
  • enum Color {
      Green,
      Yellow,
      Red,
    }
  • can have Custom variants

Pattern Matching

  • match works kind of like switch in other languages
  • can use match as an expression (like with if earlier) -- must cover every variant or use a catch all pattern or you will get a compiler error

Methods

************* functions vs methods? **************** ************* impl? ***************

  • chainable

Self

Type Parameters

Option

  • built into the standard library
  • enum Option<T> {
      None,
      Some(T),
    }

Result

Part 4: VECTORS

Vectors

  • everything must be explicitly typed

Usize

Vectors vs Arrays

  • can use a for loop with either
  • the tradeoffs here set the stage for the biggest factor in language performance because vectors have dynamic length--- comes down to heap vs stack memory

The Stack

Without a Return

With a Return

The Heap

Part 5: OWNERSHIP

Manual Memory Management

  • rust doesn't ask us to manage memory manually
  • it figures out when to deallocate memory so there are no 'use-after-free' or 'double free' issues
    • use-after_free
    • double free
  • can cause safety issues

Automatic Memory Management

Garbage Collection

  • most popular languages have this
  • has more overhead
  • can cause slowdowns while garbage collecting

How Rust Does It

  • deallocates when goes out of scope
  • if want to force it to deallocate earlier you can put things in stand alone {} -- stand alone scope for memory allocation

Ownership

  • an 'owner' is assigned to a scope -- it can be transferred (called a 'move')
  • this allows for the transfer ownership when you return something so the memory isn't deallocated when the function with the return completes

Use-after-move Compiler Error

  • once you pass ownership it's gone, but you can assign to a new variable (rebind)

.clone()

  • deep copy
  • pass clone instead so original ownership is maintained -- avoids the use-after-move compiler error
  • clones use more memory/sacrifices performance

Part 6: BORROWING

Borrowing

Mutable References

Restrictions

Slices

  • doesn't own the elements, just references them

Part 7: LIFETIMES

Lifetimes

Annotations

Elision

The Static Lifetime

Copyright © 2022