Rust 101

🦀

Program

  • Day 1
    • Demo - Installation and live coding of an API
    • Basic Rust - Presentation with code examples
    • Discussion: Where can Statnett benefit from some Rust?

 

  • Homework

 

 

Live coding

Installing Rust

# Install Rust using rustup (on Unix)

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 1) Proceed with installation (default)  <---
# 2) Customize installation
# 3) Cancel installation

Printing

// `println!` prints to the console
// The ! indicates it is a macro (metaprogramming - code that writes code)

println!("Hello, world!");

Printing

// println! supports format strings

let name = "Bob";
println!("Hello, {name}");         // These
println!("Hello, {}", name);       // are
println!("Hello, {n}", n = name);  // equivalent

Printing

let arr = [1, 2, 3];

println!("{arr:?}");  // The :? is sometimes needed when printing complex types

format!

// Format strings can also be interpreted by the `format!` macro

let name = "Bob";
let greeting = format!("Hello, {name}!");

Type annotations

let num: i32 = 5;                        // With explicit type annotation
let num = 5;                             // Rust can usually infer the type

let nums = (0..10).collect();            // But not always
// error[E0282]: type annotations needed

let nums: Vec<i32> = (0..10).collect();  // With explicit type annotation

Integers

Length Signed Unsigned
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
arch isize usize

Integers

// Ints defaults to i32

let num = 42;  // This is an i32

// If you want a different type:

let num_1: u8 = 42;
let num_2 = 42 as u8;
let num_3 = 42u8;
let num_4 = 42_u8;

assert!(num_1 == num_2 && num_2 == num_3 && num_3 == num_4);

Integers

// Be careful when narrowing!

let negative = -5_i8;
let positive = negative as u8;
println!("{positive}");  // Prints 251

Floats

// Floats only have two types: f32 and f64
// f64 is the default

let num = 42.0;

Strings

// A String is a growable, mutable, owned, UTF-8 encoded type.

let mut s1 = String::new();
s1.push_str("hello");

let s2 = String::from("hello");

let s3 = "hello".to_string();

assert_eq!(s1, s2);
assert_eq!(s2, s3);

String slices - &str

let s = "hello".to_string();

let string_slice: &str = &s[0..4];

println!("{string_slice}");
// Prints "hell"

Chars

// Single and double quotes mean different things!

let this_is_a_string_slice = "x";
let this_is_a_char = 'x';

for char in this_is_a_string_slice.chars() {
    println!("{char}",);
}

Mutability

let data = Vec::new();
data.push(1);
// error[E0596]: cannot borrow `data` as mutable,
// as it is not declared as mutable
let mut data = Vec::new();
data.push(1);

Const

// Why const, when we can create immutable variables with let?
// Constants are known at compile time, meaning the compiler can optimize.

const LOG_LEVEL: &str = "INFO";
const NUMBERS: [i32; 3] = [1, 2, 3];

Const

let (first, second, third) = (1, 2, 3);
const MORE_NUMBERS: [i32; 3] = [first, second, third];
// error[E0435]: attempt to use a non-constant value in a constant

Structs

struct PowerStation {
    name: String,
    capacity: f64,
}

let kvildal = PowerStation {
    name: "Kvildal".to_string(),
    capacity: 1240.0,
};

let tonstad = PowerStation {
    name: "Tonstad".to_string(),
    capacity: 960.0,
};
struct PowerStation {
    name: String,
    capacity: f64,
}










Structs

// A tuple struct is a struct that has unnamed fields.

struct Point(f64, f64);

let origin = Point(0.0, 0.0);
let point = Point(3.0, 4.0);

Structs

// A unit struct is a struct that has no fields.

struct NothingToSeeHere;

let nope = NothingToSeeHere;

Enums

enum Color {
    Red,
    Green,
    Blue,
}

let favorite_color = Color::Blue;

Enums

// Enums can hold data

enum Pet {
    Goldfish,
    Cat(String),
    Dog { name: String, age: u8 }
}

let first_pet = Pet::Goldfish;
let second_pet = Pet::Cat("Misty".to_string());
let third_pet = Pet::Dog { name: "Rusty".to_string(), age: 8 };

Option

I call it my billion-dollar mistake. It was the invention of the null reference in 1965.

[...]

This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

- Tony Hoare

Option

// Instead of null or None, Rust has Option:

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

Option

/// Try to find an even number in the given sequence of numbers.
fn find_even(input: Vec<i32>) -> Option<i32> {
    for num in input {
        if num % 2 == 0 {
            return Some(num);
        }
    }
    None
    // This None is not a standalone object like None in Python,
    // it is the None variant of the Option enum
}

let maybe_number = find_even(vec![1, 3, 5]);

maybe_number + 1;  // error[E0369]: cannot add `{integer}` to `Option<i32>`

Result

// Just like there's no null or None,
// there's also no exceptions in Rust.
// Instead, we have Result:

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

Result

/// Try to divide two numbers. Return Err if the divisor is zero.
fn divide(dividend: f64, divisor: f64) -> Result<f64, String> {
    if divisor == 0.0 {
        Err("Cannot divide by zero!".to_string())
    } else {
        Ok(dividend / divisor)
    }
}

let result = divide(2.0, 3.0);

Dealing with Results

// Get the value out of the Ok variant,
// or panic if the result is Err
let result = divide(2.0, 3.0).unwrap();

// Get the value out of the Ok variant,
// or panic with a custom message if the result is Err
let result = divide(2.0, 3.0).expect("Failed to divide");

// Get the value out of the Ok variant,
// or a default value if the result is Err
let result = divide(2.0, 3.0).unwrap_or(f64::INFINITY);

// Get the value out of the Ok variant,
// or return the Err to the caller
let result = divide(2.0, 3.0)?;

// The ? operator above is equivalent to:
let result = match divide(2.0, 3.0) {
    Ok(value) => value,
    Err(e) => return Err(e),
};

// PS: Try to avoid the panicking methods in production!

Collection types

let tuple = (1, 2, 3);
let arr = [1, 2, 3];
let vec = vec![1, 2, 3];

println!("tuple: {tuple:?}");
println!("arr: {arr:?}");
println!("vec: {vec:?}");

// Prints:
// tuple: (1, 2, 3)
// arr: [1, 2, 3]
// vec: [1, 2, 3]

Collection types

// Tuples can store mixed types, arrays and vectors cannot

let tuple = (1, "hello", 4.2);
let arr = [1, "hello", 4.2];  // error[E0308]: mismatched types
let vec = vec![1, "hello", 4.2];  // error[E0308]: mismatched types

Collection types

// Indexing

let tuple = (1, 2, 3);
let arr = [1, 2, 3];
let vec = vec![1, 2, 3];

println!("First tuple element: {}", tuple.0);
println!("First array element: {}", arr[0]);
println!("First vector element: {}", vec[0]);

Arrays

// Array length must be known at compile time,
// and the size can't change.

let mut arr = [1, 2, 3];
arr = [5, 6, 7];  // This is fine
arr = [1, 2];     // This is not
// error[E0308]: mismatched types
// expected an array with a fixed size of 3 elements,
// found one with 2 elements

Tuples

// The structural type of a tuple can't change

let mut tup = (1, "cat");
tup = (2, "cats");      // This is fine
tup = ("many", "cats")  // This is not
// error[E0308]: mismatched types
// 59 |     tup = ("many", "cats")  // This is not
//    |            ^^^^^^ expected integer, found `&str`

Vectors

// Vector length DOES NOT have to be known at compile time,
// and vectors can freely grow or shrink

use rand::prelude::*;

let random_numbers = thread_rng().gen_range(1..10);
let unknown_number_of_elements = vec![0; random_numbers];

HashSets

// HashSets contain unique elements

use std::collections::HashSet;

let mut set = HashSet::from([1, 2, 3]);
set.extend([2, 3, 4]);
println!("{set:?}");
// Prints: {1, 2, 3, 4}

HashSets

let set_one = HashSet::from([1, 2, 3]);
let set_two = HashSet::from([3, 4, 5]);

let in_both = set_one.intersection(&set_two);
println!("{in_both:?}");
// Prints: [3]

let in_one_of_them = set_one.union(&set_two);
println!("{in_one_of_them:?}");
// Prints: [3, 2, 1, 5, 4]

For loop

let week = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
for day in week {
    println!("{}", day);
}

Range

for number in 1..5 {
    println!("{number}");
}
// Prints 1, 2, 3, 4

for number in 1..=5 {
    println!("{number}");
}
// Prints 1, 2, 3, 4, 5

While loop

let mut user_input = get_user_input();
while user_input != "quit" {
    user_input = get_user_input();
}

loop loop

// No need for `while true`

loop {
    let user_input = get_user_input();
    if user_input == "quit" {
        break;
    }
}

Closures

// Closures are anonymous functions that can be stored in a variable.

let add_one = |x| x + 1;

println!("{:?}", add_one(1)); // Prints "2"

Closures

// Closures can capture variables from the scope
// in which they are defined.

let x = 2;

// y is a parameter, x is a captured variable
let add_x = |y| x + y;

println!("{:?}", add_x(1)); // Prints "3"

Iterators

let numbers: Vec<i32> = vec![1, 2, 3, 4, 5];

let mut it = numbers.iter();

println!("The first element is {:?}", it.next());
println!("The second element is {:?}", it.next());
println!("The third element is {:?}", it.next());

// Prints:
// The first element is Some(1)
// The second element is Some(2)
// The third element is Some(3)

Iterators

let numbers: Vec<i32> = vec![1, 2, 3, 4, 5];

let negated_odd_numbers: Vec<i32> = numbers
    .iter()
    .filter(|&x| x % 2 == 1)
    .map(|&x| -x)
    .collect();

println!("{negated_odd_numbers:?}");
// Prints [-1, -3, -5]

Ownership

// Ownership rules:
// 1. Each value in Rust has a variable that is its owner.
// 2. There can only be one owner at a time.
// 3. When the owner goes out of scope, the value will be dropped.

let s: String = "hello".to_string();

let new_s: String = s;

println!("The string is {s}");
// error[E0382]: borrow of moved value: `s`
// Ownership rules:
// 1. Each value in Rust has a variable that is its owner.
// 2. There can only be one owner at a time.
// 3. When the owner goes out of scope, the value will be dropped.







Ownership

// Ownership rules:
// 1. Each value in Rust has a variable that is its owner.
// 2. There can only be one owner at a time.
// 3. When the owner goes out of scope, the value will be dropped.

fn takes_ownership(new_string_owner: String) {
    // The string now belongs to the variable new_string_owner.
    // The string isn't returned, therefore it is simply dropped when
    // new_string_owner goes out of scope at the end of this function.
}

let s = "hello".to_string();
takes_ownership(s);
println!("The string is {s}");
// error[E0382]: borrow of moved value: `s`

Borrowing

// Borrowing happens through references.
// References are immutable by default.

fn calculate_length(s: &String) -> usize {
    s.len()
}

let mut greeting = "hello".to_string();
let len = calculate_length(&greeting);
println!("The length of {greeting} is {len}");
// Borrowing happens through references.
// References are immutable by default.

fn calculate_length(s: &String) -> usize {
    s.push_str(", world");
    // error[E0596]: cannot borrow `*s` as mutable,
    // as it is behind a `&` reference
    s.len()
}

let mut greeting = "hello".to_string();
let len = calculate_length(&greeting);
println!("The length of {greeting} is {len}");

Borrowing

// Mutable references

let mut greeting = "hello".to_string();
change(&mut greeting);
println!("The string is now {greeting}");
// The string is now hello, world

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

Borrowing

// Borrowing rules:
// 1. At any given time, you can have either one mutable reference
//    or any number of immutable references.
// 2. References must always be valid.

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

// Ownership + borrowing rules rules prevent data races
// and allow "fearless concurrency"!

Borrowing

Initially, the Rust team thought that ensuring memory safety and preventing concurrency problems were two separate challenges to be solved with different methods. Over time, the team discovered that the ownership and type systems are a powerful set of tools to help manage memory safety and concurrency problems!

- The book

Patterns

// Literal patterns

let number = 3;

match number {
    1 => println!("All is one"),
    2 => println!("Two's company"),
    3 => println!("Three's a crowd"),
    _ => println!("Undefined territory"),
}

Patterns

// Patterns can be used with `let`, `if let`, `for` loops,
// function parameters, etc.

struct Color { r: u8, g: u8, b: u8 }

let colors = [
    Color {r: 65, g: 250, b: 9},
    Color {r: 8, g: 164, b: 18},
    Color {r: 241, g: 9, b: 98},
];

for Color { r, g, b } in colors {
    println!("{r}-{g}-{b}");
}

Patterns

// Refutability: Patterns come in two forms, refutable and irrefutable.

// The pattern (x, y) is irrefutable, i.e. it will allways match:

let point = (4, 3);
let (x, y) = point;

// The pattern Some(x) is refutable, i.e. it might not match:

let maybe_a_number = Some(3);

if let Some(n) = maybe_a_number {
    println!("It's something");
} else {
    println!("There's nothing there");
};

Exhaustive pattern matching

enum Color {
    Red,
    Green,
    Blue,
}

let color = Color::Red;

match color {
    Color::Red => println!("Red"),
    Color::Green => println!("Green"),
}
// error[E0004]: non-exhaustive patterns: `Color::Blue` not covered

Exhaustive pattern matching

enum Color {
    Red,
    Green,
    Blue,
}

let color = Color::Red;

match color {
    Color::Red => println!("Red"),
    Color::Green => println!("Green"),
    _ => println!("Some other color"),
}

Exhaustive pattern matching

let num = 5;

match num {
    1 => println!("One"),
    2 => println!("Two"),
    3 => println!("Three"),
}
// error[E0004]: non-exhaustive patterns:
// `i32::MIN..=0_i32` and `4_i32..=i32::MAX` not covered

Tail expressions

fn function_one() -> String {
    do_other_stuff();
    return "This string will be returned".to_string();
}

fn function_two() -> String {
    do_other_stuff();
    "This will be returned, even though there is no return keyword".to_string()
    // Tail expression
}

fn function_three() {
    do_other_stuff();
    "This will NOT be returned due to the trailing semicolon".to_string();
}

Expressions

// In Rust, almost everything is an expression,
// meaning it evaluates to a value.
// For example, `if` is an expression, not a statement.

let answer = if 1 + 1 == 2 {
    do_stuff();
    42
} else {
    do_other_stuff();
    24
};

println!("The answer is {answer}") // Prints "The answer is 42"

Expressions

let answer = if 1 + 1 == 2 {
    match 1 + 1 {
        2 => {
            {
                {
                    {
                        42
                    }
                }
            }
        }
        _ => 24
    }
}
else {
    24
};

println!("The answer is {answer}")  // Prints "The answer is 42"

Expressions

let x: u8 = 2;
let y: u8 = 3;
let rival_team: &HashSet<(u8, u8)> = &HashSet::new();

// Everything is an expression, including block.
// How can this be used? Might help to communicate intent. Compare these two, wo are doing the exact same thing:

// As we are reading the first line below, the intent isn't necessarily immediately clear:
let mut moves = HashSet::from([(x + 1, y + 1)]);
if let Some(new_x) = x.checked_sub(1) {
    moves.insert((new_x, y + 1));
}
let capture_moves = moves
    .intersection(rival_team)
    .cloned()
    .collect::<HashSet<_>>();

// But here, we immediately understand that all the code in the block is involved in computing capture_moves:
let capture_moves = {
    let mut moves = HashSet::from([(x + 1, y + 1)]);
    if let Some(new_x) = x.checked_sub(1) {
        moves.insert((new_x, y + 1));
    }
    moves
        .intersection(rival_team)
        .cloned()
        .collect::<HashSet<_>>()
};

// An additional advantage of the latter is that the temporary variable `moves` is contained in the scope
// (it's cleared after the block has finished)

impl

struct Dog {
    name: String,
}

impl Dog {
    fn bark(&self) {
        println!("{} barks", self.name);
    }
}

let fido = Dog { name: "Fido".to_string() };

fido.bark();

Traits

trait Animal {
    fn speak(&self);
}

struct Dog;
struct Cat;
struct Fox;

impl Animal for Dog {
    fn speak(&self) {
        println!("Woof!");
    }
}

impl Animal for Cat {
    fn speak(&self) {
        println!("Meow!");
    }
}

impl Animal for Fox {
    fn speak(&self) {
        println!("Ring-ding-ding-ding-dingeringeding!");
    }
}
trait Animal {
    fn speak(&self);
}

struct Dog;
struct Cat;
struct Fox;


















Traits

fn make_animal_speak(animal: &impl Animal) {
    // This function accepts any type that implements the Animal trait.
    animal.speak();
}

let dog = Dog;
make_animal_speak(&dog);

Associated functions

struct Dog;

impl Dog {
    fn speak() {
        // Note that `speak` doesn't have a &self parameter,
        // meaning it becomes an "associated function" (think static method)
        println!("Woof!");
    }
}

// Dog.speak();  // error[E0599]: no method named `speak` found for struct
Dog::speak(); // :: is used when accessing associated functions

Modules

// Separate files are also modules, and can be
// brought in with the `mod` keyword, but 
// only from main.rs or lib.rs

mod separate_file_module;  // This only works from main.rs or lib.rs

separate_file_module::do_something();

Modules

// From files other than main.rs or lib.rs,
// use `use` instead of `mod`

use crate::separate_file_module;

separate_file_module::do_something();

Modules

// Modules can also be defined inline.
// Tests are a good use case for this.

mod inline_module {
    pub fn do_something() {               // Note the `pub`
        println!("Doing something");
    }
}

inline_module::do_something();

Tips

  • Start by modeling the problem domain. Do it in such a way that invalid states are unrepresentable
  • Prefer match over if
  • Compileren er din venn. Les feilmeldingen pÃ¥ nytt. Og pÃ¥ nytt

Hva er Rust egnet for?

  • Ofte omtalt som et systemprogrammeringssprÃ¥k, kanskje fordi det er fÃ¥ sprÃ¥k som er egnet der. Eller fordi det var det det ble markedsført som i starten. Men det har blitt et "general purpose" sprÃ¥k som egner seg til bÃ¥de høynivÃ¥ og lavnivÃ¥ programmering
  • Modent og production-ready (det er flere Rust-prosjekter pÃ¥ GitHub enn Scala, Kotlin, Swift og Perl
  • Kan f.eks. fint brukes til web (https://www.arewewebyet.org/)
  • Rust er egnet der hvor:
    • Ytelse ELLER pÃ¥litelighet / korrekthet er viktig
    • Store / komplekse kodebaser, der Rusts feilhÃ¥ndtering som en del av typesystemet kan gjøre at man lettere oppdager utfordringer som oppstÃ¥r pga. faktorisering
    • Der man ønsker "fearless concurrency"
  • Oppfordring: Test det ut. Være med Ã¥ videreutvikle rust-build og annen "scaffolding"

Links

These slides are available at

rust.danielhjerholm.me

Homework

2. Install Rust, create and run "hello, world"

1. Watch video: How To Speak Rust

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cd ~/.projects
cargo new rust-hello-world --bin
cd rust-hello-world/
cargo run