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
Rust language documentation: https://doc.rust-lang.org/book/index.html
Rust by example: https://doc.rust-lang.org/rust-by-example/
Rust users forum: https://users.rust-lang.org/
Rust advent of code: https://github.com/BurntSushi/advent-of-code
Rust installer: https://rustup.rs/
Rust IntelliJ plugin: https://intellij-rust.github.io/
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
- Recoverable errors use Option<T> and Result<T, E>
- Non-recoverable errors use panic!()
- https://blog.burntsushi.net/rust-error-handling/
Prevents race conditions via ownership and borrowing rules
- Write-access references must be exclusive
- Shared references must be read-only
- https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html
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
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
Example API implemented in Rust:
https://github.com/ComputerScienceHouse/csh-bot/blob/master/src/main.rs
Showcases:
- Http request handling
- Json serialization/deserialization
- Error handling
Why Rust matters
By Nick Mosher
Why Rust matters
- 384