i.e. why it is worth your time to learn it
Strong safety guarantees
High performance
Batteries-included tooling
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:
(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:
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
(part 2: automatic memory management)
Let's talk about memory management
Ownership
Imagine a function call-stack where every value belongs to a specific stack frame (the frame "owns" the value)
fn main
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
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
What does this model lack so far?
(part 3: borrowing)
Let's talk about this Ownership metaphor
Consider ownership as the right to destroy something
Consider borrowing as the right to use something, but not to destroy it
Rust uses Borrowing to grant access without moving
Borrowing
We can give a function temporary access to a resource
fn main
fn main() {
let greeting =
String::from("Hello, world!");
print_greeting(&greeting);
println!("{}", greeting); // OK!
}
fn print_greeting(greeting: &str) {
println!("{}", greeting);
}
fn print_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);
}
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
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
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
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: