Introduction to Rust

Slides

bit.ly/hs1910-rust

What is Rust?

A new language bringing features of modern programming languages to systems

…with a focus on safety, speed, and concurrency, and zero-cost abstractions (from C++’s zero-overhead principle).

Resources

The Book; Rust by Example

Standard library documentation

Rustonomicon; Reference

rsdoc search engine

On Firefox, you can add this as a keyword bookmark.

Installing Rust

Use Rustup.

Why Rustup?

It’s common to want to use different versions of the compiler.

Rustup also helps you install new targets (to cross-compile).

Different versions?

New features start off in Unstable Rust and “cook” for a while.

It is only possible to use unstable features using the Nightly build.

Also, you may need to test your project on older versions at times.

Cross-compilation

Rust is based on LLVM, and so cross-compilation is supported out-of-the-box.

But to target a target, you need to have the standard library for that target. Rustup helps you install those additional things.

Hello world

fn main() {
  println!("Hello world!");
}

Compile and run

$ rustc helloworld.rs
$ ./helloworld
Hello world!

Hello Cargo

Cargo is Rust’s build system and package manager.

$ cargo new --bin hello_cargo
$ cd hello_cargo

Cargo.toml

Cargo.toml is the package metadata file (akin to e.g. package.json, etc).

The format is TOML.

Cargo.toml

[package]
name = "hello_cargo"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
edition = "2018"

[dependencies]

Compiling with Cargo

Edit your program: src/main.rs

fn main() {
  println!("Hello Cargo!");
}

Compile and run

$ cargo build
$ target/debug/hello_cargo
# Or...
$ cargo run

Moving forward

Until we start using Cargo features, you can either call rustc directly, or use Cargo.

Basic syntax

fn main() {
  let x = true;
  let mut y: u32 = 1u32;
  if x {
    println!("{}", y);
    y += 1;
  }

  while y < 10 {
    y += 1;
    println!("{}", y);
  }
}

Basic syntax

fn add(i: i32, j: i32) -> i32 {
  i + j // comment
}
/* comment */
/* /* nested comment */ */
fn main() { println!("{}", add(3, 5)); }

The last expression in a block is returned.

println

println is a macro.
Macros are invoked using macro!(...) (note the !).
(We’ll go into macros in more detail later.)

Format string syntax

Of note are {}, which is the generic display,
and {:?}, which is the debug print specifier.
(We’ll demonstrate the power of {:?} later.)

Primitive scalar types [B][R]

Boolean: bool: true or false

Integers: {i, u}{8, 16, 32, 64, 128, size}
e.g. 1, 1u64; default i32

Floats: f32, f64 e.g 2f64, 2., 2.0
default f64

Unicode character: char e.g. 'a', '🤔'

Unit type: ()

Number literals [R]

Binary literals: 0b010101

Octal literals: 0o25

Hex literals: 0x15

Number separators: 123_456_789

String literals [R]

Strings: "abc"

Raw strings:

r#"hello! I am a raw string
I can contain any number of " and # and r#" and //
and \ but I end the moment we have "# // this is a comment

Also byte literals and byte string literals.

Tuples and arrays [B]

let x: (i32, i32, i32) = (1, 2, 3);
let y: [i32; 3] = [1, 2, 3];
let z = [0u32; 500]; // array of 500 0u32

Notice that the length of an array is part of its type.

Type casting [R]

Most commonly used to convert between numeric types:

let x: i32 = 5;
x as i16

Expressions and statements [R]

Rust is an expression-based language.

Most things are expressions, including if, and loops.

The only thing that is a statement is the let statement.

Block expression

All blocks are expressions.

println!("{:?}", {
  let mut y = 0;
  while y < 10 { y += 1; }
  y
});

Blocks evaluate to the last expression evaluated.

The semicolon

; converts an expression to a statement (which is of type ()), discarding the value.

println!("{:?}", {
  let mut y = 0;
  while y < 10 { y += 1; }
  y;
});

if expression [B]

if is an expression.

let x = true;
println!("{}", if x { 1 } else { 0 });

Note that both branches must evaluate to the same type (which could be ()).

while loop [B]

The while loop is an expression evaluating to ().

let mut x = 0;
println!("{:?}", while x < 10 { x += 1; });

for loop [B]

The for loop is an expression evaluating to ().

let x = [1, 2, 3];
for y in &x {
  println!("{}", y);
}

Infinite loop [B]

let mut x = 0;
println!("{}", loop {
  x += 1;
  if x > 10 { break x; }
});

loop expressions can evaluate to a non-() value!

Range expressions [R]

let x = [1, 2, 3, 4, 5];
for i in 0..x.len() {
  println!("{}", x[i]);
}
for i in 0..=4 {
  println!("{}", x[i]);
}

Macros

Rust macros are not like CPP macros.

They operate on the AST level at compile-time.

Standard library macros

Macro invocations generally look like macro!(...).

Formatting macros

Macros operate on the AST level at compile-time.

println!("Hello! {0} in hex is {0:02X}", 15);

This means that format strings are actually parsed and type-checked at compile-time,

println!("Hello! {0} in hex is {0:02X}", "Hello");
// error[E0277]: the trait bound `str: std::fmt::UpperHex`
// is not satisfied

…and are expanded out into function calls to produce the string you need.

Exercise 1

To get you familiar with the syntax:

Implement FizzBuzz. In C: (i.e. translate this to Rust)

int main() {
  for (int i = 1; i <= 100; ++i) {
    int m3 = i % 3 == 0, m5 = i % 5 == 0;
    if (m3 || m5) {
      printf("%s%s\n", m3 ? "Fizz" : "", m5 ? "Buzz" : "");
    } else {
      printf("%d\n", i);
    }
  }
}

Exercise 1

fn main() {
  for i in 1..=100 {
    let m3 = i % 3 == 0; let m5 = i % 5 == 0;
    if m3 || m5 {
      println!("{}{}",
        if m3 { "Fizz" } else { "" },
        if m5 { "Buzz" } else { "" });
    } else {
      println!("{}", i);
    }
  }
}

(Note: this isn’t the most idiomatic Rust.
We’ll get there.)

Ownership [B]

Ownership is Rust’s way of memory management.

Ownership rules

  • Each value has exactly one binding that owns it.
  • When the binding goes out of scope, the value is dropped.

String

To exemplify ownership, let’s look at the String type: a resizable string buffer.

It is akin to StringBuilder in Java/C#, or std::string in C++.

let mut s = String::from("hello");
s.push_str(", world!");
println!("{}", s);

This prints “hello, world!”.

String

String is internally a pointer to a heap buffer and a string length (plus the length of the buffer).

When we create a new String, the function we call allocates this buffer for us.

But how do we then deallocate (free) this buffer?

Dropping

{
  let mut s = String::from("hello");
  // do things with s
} // the String owned by s is dropped

s owns the String. When s goes out of scope, the value it points to is dropped.

When a value is dropped, a destructor can be called. In the case of String, it frees the heap allocation.

This is essentially RAII in C++.

Moving

{
  let s1 = String::from("hello");
  let s2 = s1;
}

When s1 and s2 go out of scope, what happens?

Moving

When we do s2 = s1, the String is moved into s2. s1 is now invalid.

{
  let s1 = String::from("hello");
  let s2 = s1;
  println!("{}", s1);
  // error[E0382]: borrow of moved value: `s1`
}

This is what is unique about Rust: values have unique ownership; this eliminates use-after-frees and double-frees.

cloneing

let s1 = String::from("hello");
let s2 = s1.clone();
println!("{}", s1);

clone is a method implemented by String that makes a deep copy.

What about primitives?

let a = 1;
let b = a;
println!("{}", a);

Scalar primitives (integers, floats, characters) implement the Copy trait.

(We’ll talk about traits later.)

drop

It is possible to manually drop a value:

let s1 = String::from("hello");
drop(s1);

Quick question: How is drop implemented?

References

Suppose we want a function that does work on a String.

fn calc_len(x: String) -> usize {
  x.len()
}

fn main() {
  let x = String::from("hello");
  println!("{}", calc_len(x));
  println!("{}", x);
  // error[E0382]: borrow of moved value: `x`
}

References

fn calc_len(x: String) -> (String, usize) {
  let len = x.len();
  (x, len)
}

fn main() {
  let x = String::from("hello");
  let y = calc_len(x);
  let x = y.0;
  let len = y.1;
  println!("{}", x);
  println!("{}", len);
}

Not very convenient.

References

Let’s pass in a reference instead.

fn calc_len(x: &String) -> usize {
  x.len()
}

fn main() {
  let x = String::from("hello");
  println!("{}", calc_len(&x));
  println!("{}", x);
}

Mutable references

fn change(some_string: &String) {
  some_string.push_str(", world");
  // error[E0596]: cannot borrow `*some_string` as mutable,
  // as it is behind a `&` reference
}

fn main() {
  let s = String::from("hello");
  change(&s);
}

Normal references are immutable.

Mutable references

fn change(some_string: &mut String) {
  some_string.push_str(", world");
}

fn main() {
  let mut s = String::from("hello");
  change(&mut s);
}

We need to make a mutable reference.

Aside: Docs

String docs

You can tell whether a method takes:

the entire value fn myfn(self)
a mutable reference fn myfn(&mut self)
a reference fn myfn(&self)

Mutable refs are exclusive

fn main() {
  let mut s = String::from("hello");
  let mutref1 = &mut s;
  let mutref2 = &mut s;
  // error[E0499]: cannot borrow `s` as mutable
  // more than once at a time
  println!("{}", mutref1);
}

Mutable refs are exclusive

fn main() {
  let mut s = String::from("hello");
  let mutref1 = &mut s;
  let mut t = s;
  // error[E0505]: cannot move out of `s`
  // because it is borrowed
  println!("{}", mutref1);
}

Mutable refs are exclusive

fn main() {
  let mut s = String::from("hello");
  let mutref1 = &mut s;
  let ref1 = &s;
  // error[E0502]: cannot borrow `s` as immutable
  // because it is also borrowed as mutable
  println!("{}", mutref1);
}

Mutable refs are exclusive

This solves data races.

It is possible to have aliasing and mutability using Cell/RefCell, or Mutex/RwLock for multithreaded scenarios.

These types do checks at runtime instead, in line with Rust’s philosophy: you opt-in to only what you need.

Slices

We’ve passed references to Strings.
What if we want a substring?

let s = String::from("hello world");

let hello: &str = &s[0..5];
let world = &s[6..11];

println!("{} {}", world.len(), world);

&str is a fat pointer: like char *, but it tracks the length too.

String literals

The type of a string literal is &str *.

let s: &str = "Hello!";
println!("{} {}", s.len(), s);

We can slice them too.

let s: &str = "Hello world!";
println!("{}", &s[..5]);

* Almost.

Array slices

You can slice arrays:

let arr: [u32; 5] = [1, 2, 3, 4, 5];
let slice: &[u32] = &arr[..3];
println!("{:?}", slice);

The type of a slice is just &[T].

Lifetimes

let r;
{
  let x = 5;
  r = &x;
  // error[E0597]: `x` does not live long enough
}
println!("r: {}", r);

x lives from line 3–6: its lifetime ends at line 6.

Therefore, any reference to x cannot be referenced past line 6.

We’ve eliminated dangling pointers.

Specifying lifetimes

Lifetimes are part of references’ types.

It is possible to name lifetimes, but we’ll only see how this is important later.

There is a special lifetime name static.

With that, we can give the full type of a string literal:

let x: &'static str = "Hello!";

Structs [B]

struct Bus {
  number: String, interval: u32
}

fn get_route(num: &str) -> Bus {
  Bus {
    number: String::from(num), interval: 5
  }
}

fn main() {
  let route = get_route("A1");
  println!("Got route: {}, interval {}",
    route.number, route.interval);
}

impling methods

Continuing from the previous example.

impl Bus {
  fn arrive(&self) {
    println!("{} has arrived", self.number);
  }
}

fn main() {
  let route = get_route("A1");
  route.arrive();
}

Update syntax

impl Bus {
  fn with_number(self, new_num: &str) -> Bus {
    Bus {
      number: String::from(new_num),
      ..self
    }
  }
}

fn main() {
  let route = get_route("A1").with_number("A2");
  route.arrive();
}

Associated methods

i.e. “static” methods

impl Bus {
  fn of_number(num: &str) -> Self {
    Bus {
      number: String::from(num), interval: 5
    }
  }
}

fn main() {
  let route = Bus::of_number("A1").with_number("A2");
  route.arrive();
}

Enums [B]

Sum types, or algebraic data types.

enum Message {
  Quit,
  Move { x: i32, y: i32 },
  Write(String),
  ChangeColor(i32, i32, i32),
}

Using enums

fn handle(x: Message) {
  // ... ???
}

fn main() {
  let x = Message::ChangeColor(0, 0, 0);
  handle(x);
}

How do we know which variant we have?

match expression [B]

let x = Message::ChangeColor(0, 0, 0);

fn handle(x: Message) {
  match x {
    Message::Quit => println!("Quitting."),
    Message::Move { x, y } => println!("Moving to {}, {}", x, y),
    Message::Write(str) => println!("Write {}", str),
    Message::ChangeColor(r, g, b) => println!("Changing colour to {}, {}, {}", r, g, b)
  };
}

match is exhaustive

fn handle(x: Message) {
  match x {
    Message::Quit => println!("Quitting."),
    Message::Move { x, y } => println!("Moving to {}, {}", x, y),
    Message::Write(str) => println!("Write {}", str),
    // Message::ChangeColor(r, g, b) => println!("Changing colour to {}, {}, {}", r, g, b)
  };
}

Comment out that line. We get:

error[E0004]: non-exhaustive patterns:
  `ChangeColor(_, _, _)` not covered

if let [B]

If we only care about one case, we can use if let:

let x = Message::ChangeColor(0, 0, 0);
if let Message::ChangeColor(r, g, b) = x {
  println!("Changing colour to {}, {}, {}", r, g, b);
}

Patterns [B][R]

if let, match, and in fact let all work on patterns.

let (x, y) = (1, 2);
println!("{} {}", x, y);

(x, y) is a tuple pattern.

Patterns

You can do the same with enums and structs.

struct Bus {
  number: String, interval: u32
}

impl Bus {
  fn of_number(num: &str) -> Self {
    Bus {
      number: String::from(num), interval: 5
    }
  }
}

fn main() {
  let route = Bus::of_number("A1");
  let Bus { interval: int, .. } = route;
  println!("Got interval {}", int);
}

Patterns

_ acts as a wildcard, since matches need to be exhaustive.

fn main() {
  let x = 7;
  match x {
    1 => println!("one"),
    3 => println!("three"),
    5 => println!("five"),
    7 => println!("seven"),
    _ => (),
  };
}

Exercise 2

Rewrite FizzBuzz with match.

Exercise 2

fn main() {
  for i in 1..=100 {
    match (i % 3, i % 5) {
      (0, 0) => println!("FizzBuzz"),
      (0, _) => println!("Fizz"),
      (_, 0) => println!("Buzz"),
      (_, _) => println!("{}", i)
    }
  }
}

Back to enums

Like structs, you can define methods on them.

enum Message {
  Quit,
  Move { x: i32, y: i32 },
  Write(String),
  ChangeColor(i32, i32, i32),
}

impl Message {
  fn is_quit(&self) -> bool {
    match self {
      Message::Quit => true,
      _ => false
    }
  }
}

fn main() {
  println!("{}", Message::Quit.is_quit());
}

Option

Instead of null, Rust has the Option type.

struct Person {
  name: Option<String>
}

impl Person {
  fn greet(&self) {
    match &self.name {
      Some(name) => println!("Hello, {}", name),
      None => println!("Hello, MISSINGNO.")
    }
  }
}

fn main() {
  Person { name: None }.greet();
}

Option methods

Option has many methods. Let’s rewrite Person::greet.

struct Person {
  name: Option<String>
}

impl Person {
  fn greet(&self) {
    println!("Hello, {}",
      self.name.as_ref().map(String::as_str)
        .unwrap_or("MISSINGNO."));
  }
}

fn main() {
  Person { name: None }.greet();
}

Result [B]

Instead of exceptions, Rust has Result.

fn might_fail(inp: i32) -> Result<i32, &'static str> {
  if inp % 3 == 0 {
    Err("failed!")
  } else {
    Ok(inp)
  }
}

fn main() {
  match might_fail(3) {
    Ok(result) => println!("Got {}", result),
    Err(err) => println!("Failed: {}", err)
  };
}

Result is a monad, and has many methods too.

Propagating errors: ? [B]

The ? operator helps to reduce boilerplate.

fn might_fail_2(inp: i32) -> Result<i32, &'static str> {
  let x = might_fail(inp)?;
  Ok(might_fail(2)? * x)
}

fn main() {
  println!("{:?}", might_fail_2(3));
  println!("{:?}", might_fail_2(4));
}

panicking [B]

Some functions are documented to panic on failure conditions.

On panic, your application will abort.

It is Rust’s way of failing fast, when there is no way to recover from an exceptional situation.

Generics [B]

Rust has generics, akin to those in Java and C#.

We’ve already seen Option and Result.
Let’s see how they are defined.

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

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

Generics

In impls:

struct Point<T> {
  x: T,
  y: T,
}

impl<T> Point<T> {
  fn x(&self) -> &T {
    &self.x
  }
}

Generics

You can impl a specific concrete type:

impl Point<f32> {
  fn distance_from_origin(&self) -> f32 {
    (self.x.powi(2) + self.y.powi(2)).sqrt()
  }
}

Generics over lifetimes

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Traits

Traits are like interfaces in Java and C#. They’re closest to Haskell typeclasses.

trait Summary {
  fn summarize(&self) -> String;
}

impling traits [B]

struct NewsArticle {
  headline: String,
  author: String,
  content: String,
}

impl Summary for NewsArticle {
  fn summarize(&self) -> String {
    format!("{}, by {}", self.headline, self.author)
  }
}

fn main() {
  let art = NewsArticle {
    headline: "Rust is great!".to_owned(),
    author: "Graydon Hoare".to_owned(),
    content: "Rust is really really good!".to_owned()
  };
  println!("{}", art.summarize());
}

impling traits

You can impl a trait you define on other types:

impl Summary for String {
  fn summarize(&self) -> String {
    (&self[..self.len().min(10)]).to_owned()
  }
}

fn main() {
  let x = String::from("Hellohello hello");
  println!("{}", x.summarize());
}

Trait bounds [B]

fn print_summary<T>(x: &T) where T: Summary {
  println!("{}", x.summarize());
}

fn main() {
  let x = String::from("Hellohello hello");
  print_summary(&x);
}

Trait bounds

fn print_summary<T: Summary>(x: &T) {
  println!("{}", x.summarize());
}

fn main() {
  let x = String::from("Hellohello hello");
  print_summary(&x);
}

Blanket impls [B]

use std::fmt::Display;

impl<T: Display> Summary for T {
  fn summarize(&self) -> String {
    let s = self.to_string();
    (&s[..s.len().min(5)]).to_owned()
  }
}

fn main() {
  print_summary(&1234567890);
}

Common traits

  • Debug: Can be printed with {:?}
  • Clone: Can be .clone()d
  • Copy: Opt-out of move semantics

#[derive]

Some traits can be auto-derived:

#[derive(Debug, Clone)]
struct Bus {
  number: String, interval: u32
}

fn main() {
  let a2 = Bus {
    number: "A2".to_owned(),
    interval: 5
  };
  let a2_2 = a2.clone();
  println!("{:?}", a2);
  println!("{:?}", a2_2);
}

Operators

Rust operators are defined using traits e.g. Add

use std::ops::Add;

#[derive(Debug, Clone, Copy)]
struct Point {
  x: i32,
  y: i32,
}

impl Add for Point {
  type Output = Self;

  fn add(self, other: Self) -> Self {
    Self {
      x: self.x + other.x,
      y: self.y + other.y,
    }
  }
}

fn main() {
  let p1 = Point { x: 5, y: 5 };
  let p2 = Point { x: -5, y: 5 };
  println!("{:?}", p1 + p2);
}

Lambdas and closures [B]

fn  add_one_v1   (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

Lambdas and closures

Closures implement (one of)

  • Fn: those that only take immutable references to captured variables
  • FnMut: those that take mutable references
  • FnOnce: those that might only be callable once

Lambdas and closures

fn apply_fn<F, T>(f: F, arg: T) -> T
  where F: Fn(T) -> T {
  f(arg)
}

fn main() {
  println!("{}", apply_fn(|x| x * 2, 5));
}

Lambdas and closures

fn apply_fn<F, T>(mut f: F, arg: T) -> T
  where F: FnMut(T) -> T {
  f(arg)
}

fn main() {
  let mut y = 10;
  println!("{}", apply_fn(|x| { y += x; y }, 5));
}

Iterators [B]

Iterators are like Streams in Java, and LINQ in C#.

You can iterate through them:

let x = [1, 2, 3, 4, 5];
for i in x.iter() {
  println!("{}", i);
}

Iterators

But their power comes from their composability:

let x = [1, 2, 3, 4, 5];
for i in x.iter().map(|x| x * x) {
  println!("{}", i);
}

Iterators

On ranges:

for i in (1..=5).map(|x| x * x) {
  println!("{}", i);
}

Iterators

for i in (1..=5).chain(21..=25).map(|x| x * x) {
  println!("{}", i);
}

Iterators

let x = ["Hello", "World", "3", "4", "5"];
for (i, s) in x.iter().enumerate() {
  println!("{} = {}", i, s);
}

crates.io

Rust’s package repository.

Adding dependencies

In Cargo.toml:

[dependencies]
rand = "0.7"

Using dependencies

Docs at docs.rs/rand

src/main.rs:

fn main() {
  for i in (1..100).map(|_| rand::random::<i32>()) {
    println!("{}", i);
  }
}

Common crates

Documentation [B]

You can make documentation comments using ///:

/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
    x + 1
}

Documentation

You can render the documentation:

$ cargo doc

Architecting Rust programs

Some high-level tips:

  • Model programs as a flow of data
    • Like functional programs
    • Avoid shared mutable data, where possible
    • Have a logical ownership hierarchy

Architecting Rust programs

  • Decide on & vs &mut from the caller’s perspective
    • Expect to have many threads calling it? Use & and handle mutability internally
    • Expect to have only one thread? Use &mut to enforce that guarantee

Architecting Rust programs

  • If you’re fighting the borrow checker, something is not right

Where to go from here?

Thank you!

Please fill in the feedback form:

bit.ly/hs-2019-rust-fb