Why Rust matters

i.e. why it is worth your time to learn it

Strong safety guarantees

High performance

  • Compiles to machine code (backed by LLVM)
  • No garbage collector (almost no runtime)

Batteries-included tooling

  • Package manager Cargo installed by default
  • Plugins for popular editors (Intellij, VsCode, ...)

High-quality ecosystem

High-quality documentation

Highly active community

At a glance

Some resources:

Safety Guarantees

Memory-safe References

(part 1: data races)

Data Races

A Data Race occurs when two pointers access the same memory at once, and at least one of those accesses is a mutation.

Two elements: Shared Memory and Mutable Memory

To eliminate data races, Rust has two reference types:

  • Shared immutable references (&T)
  • Exclusive mutable references (&mut T)
use std::thread;

fn main() {
    let mut counter = 0;

    thread::spawn(|| {
        for _ in 0..10 {
            counter += 1;
        }
    });

    // What should this print?
    println!("Counter is {}!", counter);   
}

Let's look at some unsound code

In Rust, this is a compile-time error. If it were allowed to compile, it would be undefined behavior.

Rust shows us exactly why this isn't allowed to compile

Safety Guarantees

Memory-safe References

(part 2: automatic memory management)

Manual Memory

  • Programmer is responsible for cleaning heap memory
  • If you free memory too few times, you get a memory leak
  • If you free memory too many times, you get a double-free and segfault

Garbage Collection

  • Runtime is responsible for cleaning heap memory
  • Introduces the stop-the-world problem
  • Makes garbage-collected languages a poor fit for real-time or high-performance applications

Let's talk about memory management

Rust uses a memory model called Ownership

Ownership

Imagine a function call-stack where every value belongs to a specific stack frame (the frame "owns" the value)

fn main

  • greeting = "Hello world!"
fn main() {
    let greeting = 
        String::from("Hello, world!");
}

Ownership

In Rust, a "call-by-value" is said to move the value into the called function

fn main

fn main() {
    let greeting = 
        String::from("Hello, world!");
        
    print_greeting(greeting);
}

fn print_greeting(greeting: String) {
    println!("{}", greeting);
}

fn print_greeting

  • greeting = "Hello world!"

Ownership

When a function returns, it drops any values in its scope

fn main

fn main() {
    let greeting = 
        String::from("Hello, world!");
        
    print_greeting(greeting);
    //             -------- value moved here
    println!("{}", greeting);
    //             ^^^^^^^^ value borrowed here after move
}

fn print_greeting(greeting: String) {
    println!("{}", greeting);
}

Ownership

This model correctly automatically cleans up memory when it goes out of scope

  • No risk of double-frees
  • No risk of forgetting to "drop" a value

What does this model lack so far?

  • All values seem to be consumed on first function call
  • How can we re-use values so we can pass them to multiple functions?

Safety Guarantees

Memory-safe References

(part 3: borrowing)

Let's talk about this Ownership metaphor

Consider ownership as the right to destroy something

  • If I own a book, I have the right to throw it away

Consider borrowing as the right to use something, but not to destroy it

  • If I lend you a book, you have the right to read it, but not the right to throw it away
  • I may tell you to only read it, not to write in it
  • When I lend you something, I eventually expect it back

Rust uses Borrowing to grant access without moving

Borrowing

We can give a function temporary access to a resource

fn main

  • greeting = "Hello world!"
fn main() {
    let greeting = 
        String::from("Hello, world!");
        
    print_greeting(&greeting);
    
    println!("{}", greeting); // OK!
}

fn print_greeting(greeting: &str) {
    println!("{}", greeting);
}

fn print_greeting

  • greeting =

Borrowing

Every reference in Rust has a lifetime, which describes how long it has access to the resource

fn main() {
    let greeting =                      // -|
        String::from("Hello, world!");  //  |--- greeting: 'a
                                        //  |
    print_greeting(&greeting);          //  |
                                        //  |
    println!("{}", greeting); // OK!    // -|
}

// Expanded lifetimes. Every & implicitly has some lifetime (&'a)
//
// We say the reference "has lifetime a" or "lives as long as a".
fn print_greeting<'a>(greeting: &'a str) {
    println!("{}", greeting);
}

Data Structures

Structs

A struct is a conjunctive data type

struct Person {
    first_name: String,
    last_name: String,
}

impl Person {
    pub fn new(first_name: String, last_name: String) -> Person {
        Person {
            first_name: first_name,
            last_name: last_name,
        }
    }
    
    pub fn say_name(&self) {
        println!("Hi, my name is {} {}",
            self.first_name,
            self.last_name);
    }
}

Associated behavior is defined in an impl block

fn main() {
    let nick = Person::new(
        "Nick".to_string(),
        "Mosher".to_string());
    
    nick.say_name();         // Method syntax
    Person::say_name(&nick); // Associated function syntax
}

Functions that use some form of self can be called with method syntax

Data Structures

Enums

An enum is a disjunctive data type

enum Commute {
    Walk,
    Bike,
    Bus,
    Car,
}

fn main() {
    let my_commute = Commute::Walk;
    match my_hometown {
        Commute::Walk => println!("I commute by walking!"),
        Commute::Bike => println!("I commute by biking!"),
        Commute::Bus => println!("I commute on the Bus!"),
        Commute::Car => println!("I commute by Car!"),
    }
}

An enum value is exactly one of the variants. You check which one it is by pattern matching

An enum forces exhaustive patterns

enum Commute {
    Walk,
    Bike,
    Bus,
    Car,
}

fn main() {
    let my_commute = Commute::Walk;
    match my_commute {
    //    ^^^^^^^^^^ patterns `Bike`, `Bus` and `Car` not covered
        Commute::Walk => println!("I commute by walking!"),
    }
    
    match my_commute {
        Commute::Walk => println!("I commute by walking!"),
        _ => println!("I commute some other way"), // Catch-all
    }
}
enum Commute { Walk, Bike, Train { have_bike: bool } }
enum CommuteEvent { GetOnBike, GetOnTrain, LeaveVehicle }

impl Commute {
    pub fn change_commute(&mut self, event: CommuteEvent) {
        use {Commute::*, CommuteEvent::*};
        match (&self, event) {
            (Walk, GetOnBike) => *self = Bike,
            (Walk, GetOnTrain) => *self = Train { have_bike: false },
            (Walk, LeaveVehicle) => println!("You're already walking!"),
            (Bike, GetOnBike) => println!("You're already on a bike!"),
            (Bike, GetOnTrain) => *self = Train { have_bike: true },
            (Bike, LeaveVehicle) => *self = Walk,
            (Train { .. }, GetOnTrain) =>
                println!("You're already on the train!"),
            (Train { .. }, GetOnBike) =>
                println!("You can't get on your bike on the train!"),
            (Train { have_bike }, LeaveVehicle) if *have_bike => *self = Bike,
            (Train { have_bike }, LeaveVehicle) if !*have_bike => *self = Walk,
            _ => unreachable!(),
        }
    }
}

Example making a state machine with enums as state

Strong safety guarantees

More compile-time rules mean fewer runtime errors

  • "If it compiles, it's probably right"

No such thing as Null. No exceptions

Prevents race conditions via ownership and borrowing rules

Strong safety guarantees: pattern matching

Enums are a closed set of possible variants

let colors = vec!["red", "green", "blue"];

let third: Option<&str> = colors.get(2);
match third {
    Some(color) => println!("The color is {}!", color),
    None => println!("There was no color"),
}

let fourth: Option<&str> = colors.get(3);
match fourth {
    Some(color) => println!("The color is {}!", color),
} // Compile Error! Non-exhautive match
// An Option represents the possibility of absence
pub enum Option<T> {
    None,
    Some(T),
}

Strong safety guarantees: fearless concurrency

Mutable references must be exclusive

use std::thread;

fn main() {
    let mut counter = 0;

    thread::spawn(|| {
        for _ in 0..10 {
            counter += 1;
        }
    });

    loop {
        println!("Counter is {}!", counter);
    }
} // Compile-time error!

High performance, low overhead

Rust follows the idea of zero-cost abstractions

Can use "unsafe" Rust to get lower-level control

No garbage collector

High performance, low overhead

ripgrep: A recursive text-searching tool like grep

  • Usually faster than grep

actix/actix-web: Actor system and web framework

tantivy/toshi: Search services similar to elasticsearch

Batteries-included tooling: Cargo

How to install Rust and start a new project:

# Install Rust
$ curl https://sh.rustup.rs -sSf | sh

# Create a new project
$ cargo new --bin hello_world
$ cd hello_world
$ ls -la
drwxr-xr-x   9 user  group  288 Jan 31 14:10 .git
-rw-r--r--   1 user  group   19 Jan 31 14:09 .gitignore
-rw-r--r--   1 user  group  133 Jan 31 14:09 Cargo.toml
drwxr-xr-x   3 user  group   96 Jan 31 14:09 src

# Run main
$ cargo run
Hello, world!

Helpful compiler messages

// src/main.rs
fn main() {
    let greeting: String = "Hello, world!";
    println!("My greeting is {}", greeting);
}

High quality ecosystem

rayon: A safe parallel processing library

crossbeam: Fast concurrency primitives

tokio: Asynchronous IO using Futures

serde: Serialization and deserialization library

More examples at awesome-rust

diesel: ORM and query builder for safe database access

Example project: Dialogflow bot

Dialogflow is a natural-language processing service that uses webhooks to send intents to a custom-built API

https://dialogflow.com/docs/fulfillment/how-it-works

Showcases:

  • Http request handling
  • Json serialization/deserialization
  • Error handling
Made with Slides.com