Control Flow

let value = 5;

if value < 10 {
    println!("Value is under 10");
} else if value == 10 {
    println!("Value is 10");
} else {
    println!("Value is over 10");
}

if / else

let value = 5;

if value {
    println!("Value is not zero");
}

condition must be boolean

let name = "Mary";

if name == "Bob" || name == "Bryan" {
    println!("Name starts with a B!");
} else if name != "Ben" && !(name == "Jerry") {
    println!("Is not Ben or Jerry");
}

Boolean operators

let number = -14;

let is_positive = if number >= 0 {true} else {false};

println!("is_positive: {}", is_positive);

Variable binding

Loops

  • loop
  • while
  • for
let mut count: u64 = 0;

loop {
    count += 1;
    if count > 1000000 {break}
}

println!("count: {}", count);

loop

let mut count: u64 = 0;

let count = loop {
    count += 1;
    if count > 1000000 {break count}
};

println!("count: {}", count);

returning values from loop

'outer: loop {
    while true {
        break 'outer;
    }
}

loop labels

let mut cond = true;

while cond {
    cond = !cond;
}

println!("{}", cond);

while

let arr = [1,2,3];

for el in arr {
    println!("{}", el*2);
}

for i in 0..10 {
    println!("{}", i);
}

for

Pattern matching

Pattern matching

match SCRUTINEE {
    PATTERN => EXPRESSION,
    PATTERN => {
        EXPRESSION BLOCK
    }
}
let a = 1;

match a {
    1 => println!("One"),
    2 => println!("Two"),
    i32::MIN..=i32::MAX => println!("Neither one nor two"),
}

// is equivalent to
if a == 1 {
    println!("One");
} else if a == 2 {
    println!("Two")
} else {
    println!("Neither one nor two");
}

Example

let a = 1;

match a {
    1 => println!("One"),
    2 => println!("Two"),
}

match is exhaustive!

let a = 1;

match a {
    1 => println!("One"),
    2 => println!("Two"),
    i32::MIN..=i32::MAX => println!("Neither one nor two")
}
let a = 1;

match a {
    1 => println!("One"),
    2 => println!("Two"),
    _ => println!("Neither one nor two")
}
let a = 1;

match a {
    1 => println!("One"),
    2 => println!("Two"),
    .. => println!("Neither one nor two")
}
let a = 51;

match a {
    1 => println!("One"),
    2 => println!("Two"),
    n => println!("Neither one nor two, but {}", n)
}

matching named variables

let a = 51;

match a {
    1 => println!("One"),
    2 => println!("Two"),
    _ => println!("Neither one nor two, but {}", a)
}

why not just print a directly?

let a = 51;

match a + 1 {
    1 => println!("One"),
    2 => println!("Two"),
    n => println!("Neither one nor two, but {}", n)
}
let a = 51;

match a + 1 {
    1 => println!("One"),
    2 => println!("Two"),
    n => println!("Neither one nor two, but {}", n)
}

// is equivalent to
if a + 1 == 1 {
    println!("One");
} else if a + 1 == 2 {
    println!("Two");
} else {
    println!("Neither one nor two, but {}", a + 1);
}
let a = 51;

match a {
    1..5 => println!("one to four"),
    5..=10 => println!("five to ten"),
    1 | 2 | 3 | 4 => unreachable!(),
    _ => println!("Anything else")
}

ranges, logical operators

match 3 {
    1..=10 => println!("Between 1 and 10, but don't know exactly"),
    _ => ()
}

match 3 {
    n @ 1..=10 => println!("{} is between 1 and 10", n),
    _ => ()
}

variable binding with @

match 3 {
    1..=10 => println!("Between 1 and 10, but don't know exactly"),
    _ => ()
}

match 3 {
    n @ 1..=10 => println!("{} is between 1 and 10", n),
    _ => ()
}
match 3 {
    n if n >= 0 => println!("Positive number"),
    n if n < 0 => println!("Negative number")
}

match guards

match 3 {
    n if n >= 0 => println!("Positive number"),
    n if n < 0 => println!("Negative number"),
    _ => ()
}
match 3 {
    n if n >= 0 => println!("Positive number"),
    n if n < 0 => println!("Negative number"),
    _ => unreachable!()
}
let rgb = (255, 255, 255);

match rgb {
    (255, _, _) => println!("Max red!"),
    (_r, 255, _b) => println!("Max green!"),
    (0, 0, b) => println!("Only blue, brightness: {:.2}", b as f64 / 255 as f64),
    (r, g, b) => println!("r: {}, g: {}, b: {}", r, g, b),
}

Destructuring

// result from a video game speedrun attempt
enum SpeedRunResult {
    Failed,
    Aborted{
        seconds: f64,
        reason: String
    },
    Finished(f64),
}


match result {
    SpeedRunResult::Failed => println!("Game over!"),
    SpeedRunResult::Aborted{seconds: s, reason: r} => println!("Aborted at {}s because {}", s, r),
    SpeedRunResult::Finished(seconds) => println!("{}s is an excellent time!", seconds),
}

Matching enums

// result from a video game speedrun attempt
enum SpeedRunResult {
    Failed,
    Aborted{
        seconds: f64,
        reason: String
    },
    Finished(f64),
}


let result = SpeedRunResult::Aborted{
    seconds: 134.42, 
    reason: String::from("failed to jump over the koopa")
};


match result {
    SpeedRunResult::Failed => println!("Game over!"),
    SpeedRunResult::Aborted{seconds: s, reason: r} => println!("Aborted at {}s because {}", s, r),
    SpeedRunResult::Finished(seconds) => println!("{}s is an excellent time!", seconds),
}

Matching enums

if let / while let

let rgb = (255, 255, 255);

if let (255, _, _) = rgb {
    println!("Max red!");
}

// is equivalent to
match rgb {
    (255, _, _) => println!("Max red!"),
    _ => (),
}

if let

while let Pattern = Scrutinee {
    BlockExpression
}
let rgb = (255, 255, 255);


if let (255, _, _) = rgb {
    println!("Max red!");
} else {
    println!("Not max red..");
}

// is equivalent to
match rgb {
    (255, _, _) => println!("Max red!"),
    _ => println!("Not max red..")
}

else can be used with if let

while let Pattern = Scrutinee {
    BlockExpression
}

while let

fn is_divisible_by(val: i64, div: i64) -> bool {
	val / div == 2
}

let mut a = 1;

while let false = is_divisible_by(a, 7) {
     a += 1;
}

println!("{}", a);

Error handling

panic!

  • when a program reaches an unrecoverable state
  • terminates the program and gives feedback
    • unwinds the stack like C++
  • there’s no way to recover *

panic! example

fn main() {
    panic!("Best program ever");
}

panic! example

fn main() {
    let _var = true || panic!("Best program ever");
}

panic! example

fn main() {
    let _var = true && panic!("Best program ever");
}

Result enum

enum Result<T, E> {
    Ok(T),
    Err(E),
}
  • std::result - included in global namespace
    • Result, Ok(T), Err(e)
  • ignoring Result gives compiler warning

Handling errors

  1. properly handle error
  2. ignore error (panic on error)
  3. propagate error up to the caller

1. Properly handle error

use std::fs::File;

fn main() {
    let f = File::open("rust_workshop_notes.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => panic!("Could not open file: {:?}", error),
    };
}

2. Ignore error (panic on error)

use std::fs::File;

fn main() {
    let f = File::open("rust_workshop_notes.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => panic!("Could not open file: {:?}", error),
    };
}
use std::fs::File;

fn main() {
    let f = File::open("rust_workshop_notes.txt").unwrap();

    /*let f = match f {
        Ok(file) => file,
        Err(error) => panic!("Could not open file: {:?}", error),
    };*/
}
  • unwrap (panic on error)
  • expect (panic with message)
use std::fs::File;

fn main() {
    let f = File::open("rust_workshop_notes.txt").expect("Could not open file");

    /*let f = match f {
        Ok(file) => file,
        Err(error) => panic!("Could not open file: {:?}", error),
    };*/
}





fn main() {
    let num_str = "one";
    let fallback_num = 0;

    let num: i32 = num_str.parse().unwrap_or(fallback_num);

    println!("{}", num);
}

2. Ignore error (default value on error)

3. Propagate error up to the caller

use std::num::ParseIntError;

type Point = (i32, i32);

fn parse_coordinate(x: &str, y: &str) -> Result<Point, ParseIntError> {
    Ok( (x.parse::<i32>()?, y.parse::<i32>()?) )
}

fn main() {
    let parsed: Point = parse_coordinate("1", "2").unwrap();
    
    println!("{:?}", parsed);
}

3. Propagate error up to the caller

use std::num::ParseIntError;

type Point = (i32, i32);

fn parse_coordinate(x: &str, y: &str) -> Result<Point, ParseIntError> {
    Ok( (x.parse::<i32>()?, y.parse::<i32>()?) )
}

fn main() {
    let parsed: Point = parse_coordinate("one", "two").unwrap();
    
    println!("{:?}", parsed);
}

3. Propagate error up to the caller

use std::num::ParseIntError;

type Point = (i32, i32);
type ParseResult<T> = Result<T, ParseIntError>;

fn parse_coordinate(x: &str, y: &str) -> ParseResult<Point> {
    Ok( (x.parse::<i32>()?, y.parse::<i32>()?) )
}

fn main() {
    let parsed: Point = parse_coordinate("one", "two").unwrap();
    
    println!("{:?}", parsed);
}

3. Propagate error up to the caller

use std::num::ParseIntError;

fn parse_and_double(s: &str) -> Result<i32, ParseIntError> {
    match s.parse::<i32>() {
        Ok(i) => Ok(i*2),
        Err(e) => Err(e)
    }
}

fn parse_and_double_2(s: &str) -> Result<i32, ParseIntError> {
    Ok(s.parse::<i32>()? * 2)
}

fn main() {
    println!("{:?}", parse_and_double("2"));
    println!("{:?}", parse_and_double_2("2"));

    println!("{:?}", parse_and_double_2("house"));
    println!("{:?}", parse_and_double_2("2").unwrap());
}

3. Propagate error up to the caller

use std::num::ParseIntError;

fn parse_and_double(s: &str) -> Result<i32, ParseIntError> {
    let res = match s.parse::<i32>() {
        Ok(i) => Ok(i*2),
        Err(e) => Err(e)
    };
    
    println!("Inside parse_and_double: {:?}", res);
    res
}

fn parse_and_double_2(s: &str) -> Result<i32, ParseIntError> {
    let res = Ok(s.parse::<i32>()? * 2);
    
    println!("Inside parse_and_double_2: {:?}", res);
    res
}

fn main() {
    parse_and_double("2");
    parse_and_double_2("2");

    parse_and_double("house");
    parse_and_double_2("house");
}

3. Propagate error up to the caller

use std::num::ParseIntError;

fn parse_and_double(s: &str) -> Result<i32, ParseIntError> {
    let res = match s.parse::<i32>() {
        Ok(i) => Ok(i*2),
        Err(e) => Err(e)
    };
    
    println!("Inside parse_and_double: {:?}", res);
    res
}

fn parse_and_double_2(s: &str) -> Result<i32, ParseIntError> {
    let res = Ok(s.parse::<i32>()? * 2);
    
    println!("Inside parse_and_double_2: {:?}", res);
    res
}

fn main() {
    parse_and_double("2");
    parse_and_double_2("2");

    parse_and_double("house");
    parse_and_double_2("house");
}

3. Propagate error up to the caller

use std::num::ParseIntError;

fn parse_and_double(s: &str) -> Result<i32, ParseIntError> {
    let res = match s.parse::<i32>() {
        Ok(i) => i,
        Err(e) => return Err(e)
    };
    
    let res = Ok(res*2);
    
    println!("Inside parse_and_double: {:?}", res);
    res
}

fn parse_and_double_2(s: &str) -> Result<i32, ParseIntError> {
    let res = s.parse::<i32>()?;
    
    let res = Ok(res*2);
    
    println!("Inside parse_and_double_2: {:?}", res);
    res
}

fn main() {
    parse_and_double("2");
    parse_and_double_2("2");

    parse_and_double("house");
    parse_and_double_2("house");
}

Propagating multiple errors

  1. collect errors using enum
  2. box the error

1. Collect errors using enum

#[derive(PartialEq, Debug)]
enum ParseOrDoubleError {
    ParseIntError,
    DivideByZero,
}

fn parse_and_divide(s: &str) -> Result<i32, ParseOrDoubleError> {
    let i = s.parse::<i32>();
    match i {
        Err(_e) => Err(ParseOrDoubleError::ParseIntError),
        Ok(0) => Err(ParseOrDoubleError::DivideByZero),
        Ok(non_zero) => Ok(1000 / non_zero)
    }
}

fn main() {
    println!("2: {:?}", parse_and_divide("2"));
    println!("2: {:?}", parse_and_divide("0"));

    println!("2: {:?}", parse_and_divide("house"));
}

2. Box the error

use std::fs::File;
use std::error::Error; // Error is a Trait

fn parse_and_open(number: &str, filepath: &str) -> Result<(), Box<dyn Error>> {
    number.parse::<i32>()?;
    File::open(filepath)?;
    Ok(())
}

fn main() {
    println!("{:?}", parse_and_open("2", "rust_workshop_notes.txt"));
    println!("{:?}", parse_and_open("two", "rust_workshop_notes.txt"));
}

Custom error

  • Represents different errors with the same type
  • Presents nice error messages to the user
  • Is easy to compare with other types
    • Good: Err(EmptyVec)
    • Bad: Err("Please use a vector with at least one element".to_owned())
  • Can hold information about the error
    • Good: Err(BadChar(c, position))
    • Bad: Err("+ cannot be used here".to_owned())
  • Composes well with other errors
https://doc.rust-lang.org/rust-by-example/error/multiple_error_types/define_error_type.html

Custom error

use std::fmt;
use std::error::Error;

#[derive(Debug, Clone)]
struct DivideByZeroError;

impl Error for DivideByZeroError {}

impl fmt::Display for DivideByZeroError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "divide by zero error")
    }
}

fn parse_and_divide(s: &str) -> Result<i32, Box<dyn Error>> {
    let i = s.parse::<i32>()?;
    match i {
        0 => Err(Box::new(DivideByZeroError)),
        non_zero => Ok(1000 / non_zero)
    }
}

fn main() {
    println!("2: {:?}", parse_and_divide("2"));
    println!("2: {:?}", parse_and_divide("0"));

    println!("2: {:?}", parse_and_divide("house"));
}

Custom error

use std::fmt;
use std::error::Error;

#[derive(Debug, Clone)]
struct DivideByZeroError;

impl Error for DivideByZeroError {}

impl fmt::Display for DivideByZeroError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "divide by zero error")
    }
}

fn parse_and_divide(s: &str) -> Result<i32, Box<dyn Error>> {
    let i = s.parse::<i32>()?;
    match i {
        0 => Err(Box::new(DivideByZeroError)),
        non_zero => Ok(1000 / non_zero)
    }
}

fn main() {
    println!("2: {:?}", parse_and_divide("2"));
    println!("2: {:?}", parse_and_divide("0"));

    println!("2: {:?}", parse_and_divide("house"));
}

Applications vs Libraries

  • Application
    • Error types are not so important
    • Errors will output to stdout/stderr
    • crates: anyhow
  • Library
    • Used by other programs
    • Errors are part of library API
    • crates: snafu, thiserror

crate anyhow

use anyhow::Error;

fn read_number_file(path: &str) -> Result<u32, Error> {
    let string = std::fs::read_to_string(path)?;
    let number = string.parse()?;
    Ok(number)
}

fn main() {
    let number = read_number_file("my_number.txt").unwrap();
    println!("Number from file: {}", number);
}

The Option enum

 no null, nil, or undefined!

Option enum

enum Option<T> {
    Some(T),
    None,
}
  • std::option::Option - included in global namespace
    • Option, Some(T), None
  • ignoring Option gives compiler warning

C++

std::optional<T>

Rust C++
return None; return std::nullopt;
return Some(foo); return foo;
is_some() operator bool()
has_value()
unwrap() value()
unwrap_or(bar) value_or(bar)

Handling errors options

  1. properly handle error option
  2. ignore error option (panic on error None)
  3. propagate error None up to the caller

1. Properly handle option

struct Satelite {
    distance_from_earth: Option<u32>
}

fn main() {
    let sat = Satelite{distance_from_earth: Some(400)};
    
    let in_orbit = match sat.distance_from_earth {
        Some(d) if d <= 22_223 => true,
        _ => false
    };
    
    println!("In orbit: {}", in_orbit);
}

2. Ignore option (panic on None)

  • unwrap (panic on None)
  • expect (panic with message)
struct Satelite {
    distance_from_earth: Option<u32>
}

fn main() {
    let sat = Satelite{distance_from_earth: Some(400)};
    
    let in_orbit = sat.distance_from_earth.unwrap() <= 22_223;
    
    println!("In orbit: {}", in_orbit);
}

2. Ignore option (panic on None)

  • unwrap (panic on None)
  • expect (panic with message)
struct Satelite {
    distance_from_earth: Option<u32>
}

fn main() {
    let sat = Satelite{distance_from_earth: None};
    
    let in_orbit = sat.distance_from_earth.expect("Distance unknown") <= 22_223;
    
    println!("In orbit: {}", in_orbit);
}

3. Propagate None up to caller

fn adds_five(number: Option<u8>) -> Option<u8> {
    Some(number?.saturating_add(5))
}

fn main() {
    let n: Option<u8> = Some(253);
    
    let n = adds_five(n).unwrap();
    
    println!("{:?}", n);
}
fn adds_five(number: Option<u8>) -> Option<u8> {
    Some(number?.saturating_add(5))
}

fn main() {
    let n: Option<u8> = None;
    
    let n = adds_five(n).unwrap();
    
    println!("{:?}", n);
}

Useful methods

  • Result, Option -> boolean
    • is_ok(), is_err()
    • is_some(), is_none()
  • Result -> Option
    • ok(), err()
use std::fs::File;

fn main() {
    let f = File::open("rust_workshop_notes.txt");
    
    println!("f is_ok: {}", f.is_ok());
    println!("f is_err: {}", f.is_err());
    
    let option = f.ok();
    println!("option: {:?}", option);
    
    println!("option is_some: {}", option.is_some());
    println!("option is_none: {}", option.is_none());
}
// use std::fs::File;

fn main() {
    // let f = File::open("rust_workshop_notes.txt");
    let f: Result<&str, &str> = Ok("some data");
    
    println!("f is_ok: {}", f.is_ok());
    println!("f is_err: {}", f.is_err());
    
    let option = f.ok();
    println!("option: {:?}", option);
    
    println!("option is_some: {}", option.is_some());
    println!("option is_none: {}", option.is_none());
}
use std::num::ParseIntError;

type Point = (i32, i32);

fn parse_coordinate(x: &str, y: &str) -> Result<Point, ParseIntError> {
    Ok( (x.parse::<i32>()?, y.parse::<i32>()?) )
}

fn main() {
    let parsed: Point = parse_coordinate("1", "2").unwrap();
    
    println!("{:?}", parsed);
}

Refactor parse_coordinate to return Option instead of Result



type Point = (i32, i32);

fn parse_coordinate(x: &str, y: &str) -> Option<Point> {
    Some( (x.parse::<i32>().ok()?, y.parse::<i32>().ok()?) )
}

fn main() {
    let parsed: Point = parse_coordinate("1", "2").unwrap();
    
    println!("{:?}", parsed);
}

Refactor parse_coordinate to return Option instead of Result