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
- properly handle error
- ignore error (panic on error)
- 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
- collect errors using enum
- 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"));
}
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);
}
Applications vs Libraries
The Option enum
no null, nil, or undefined!
Option enum
enum Option<T> {
Some(T),
None,
}
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
- properly handle
erroroption - ignore
erroroption (panic onerrorNone) - propagate
errorNone 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
Rust
By Sebastian Roll
Rust
- 292