Equality & Ordering Comparing Enums
Comparison Operators
- Enums can be compared using equality operators
- Derivable traits enable enums to be compared
- PartialEq
- Provides equality
- PartialOrd
- Provides ordering: greater/less than
- PartialEq
- PartialOrd requires PartialEq to be implemented
- Usually don’t need to manually implement
Example PartialEq
#[derive(PartialEq)]
enum Floor {
ClientServices,
Marketing,
Ops,
}
let first = Floor::ClientServices;
let second = Floor::Marketing;
if first == second {
//...
}
Example PartialOrd
#[derive(PartialEq, PartialOrd)]
enum Floor {
ClientServices,
Marketing,
Ops,
}
fn is_below(this:& Floor, other: &Floor) -> bool {
this < other
}
Example PartialOrd w/Variant Data
#[derive(PartialEq, PartialOrd)]
enum Tax {
Flat(f64),
None,
Percentage(f64),
}
fn smallest_amount(tax: Tax, other: Tax) -> Tax {
if tax < other {
tax
} else {
other
}
}
fn main() {
// Flat is always be less than None
let no_tax = Tax::None;
let flat_tax = Tax::Flat(5.5);
let ret = smallest_amount(no_tax, flat_tax);
match ret {
Tax::None => println!("Tax::None < Tax::Flat(5.5)"),
Tax::Flat(_x) => println!("Tax::None > Tax::Flat(5.5)"),
_ => (),
}
// Flat is always be less than Percentage
let flat_tax = Tax::Flat(4.0);
let percent = Tax::Percentage(1.0);
let ret = smallest_amount(flat_tax, percent);
match ret {
Tax::Flat(_x) => println!("Tax::Flat(4.0) < Tax::Percentage(1.0)"),
Tax::Percentage(_x) => println!("Tax::Flat(4.0) > Tax::Percentage(1.0))"),
_ => (),
}
// compare the actual variance
let low = Tax::Flat(5.5);
let high = Tax::Flat(8.0);
let ret = smallest_amount(low, high);
match ret {
Tax::Flat(x) => {
if x == 5.5 {
println!("Tax::Flat(5.5) < Tax::Flat(8.0)")
} else if x == 8.0 {
println!("Tax::Flat(5.5) > Tax::Flat(8.0)")
}
}
_ => (),
}
}
result
Tax::None > Tax::Flat(5.5)
Tax::Flat(4.0) < Tax::Percentage(1.0)
Tax::Flat(5.5) < Tax::Flat(8.0)
Recap
- Enums can be ssorted and compared
- PartialOrd and PartialEq implementation required
- These traits can be used with derive
- Ordering respects enum variant order in the code
- Variant data will only be considered for ordering if both enumerations are the same variant
- Manual implementation almost never needed for enums
Equality & Odering Comparing Structs
Comparison Operators
- Structs can be compared using equality operator
- Detivable traits enable structs to be compared
- PartialEq
- Provides equality
- PartialOrd
- Provides ordering: greater/less than
- PartialEq
- PartialOrd requires PartialEq to be implemented
Example PartialEq
#[derive(PartialEq)]
struct User {
id: i32,
name: String,
}
// when use PartialEq
// all the struct field should be the same
let a = User {id:1, name:“a”.to_owned()};
let b = User {id:2, name:“b”.to_owned()};
if a == b {
//…
}
**Example PartialOrd**
#[derive(PartialEq, PartialOrd)]
struct User {
id: i32,
name: String,
}
// when use PartialOrd
// only the first field of the structure is compared
let a = User {id:1, name:"a".to_owned()};
let b = User {id:2, name:"b".to_owned()};
if a < b {
// just compare id
//...
}
PartialOrd
- PartialOrd only considers the first struct fiels
- Manual implementation needed to compare other fileds
- Always ensure PartialOrd and PartialEq are consistent
PartialOrd Manual implementation
Example01
use std::cmp::Ordering;
#[derive(PartialEq)]
struct User {
id: i32,
name: String,
}
impl PartialOrd for User {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if self.name < other.name {
Some(Ordering::Less)
} else if self.name > other.name {
Some(Ordering::Greater)
} else {
Some(Ordering::Equal)
}
}
}
fn main() {
let a = User {
id: 1,
name: "a".to_owned(),
};
let b = User {
id: 2,
name: "b".to_owned(),
};
if a < b {
println!("a < b");
}
}
Example02
use std::cmp::Ordering;
#[derive(PartialEq)]
struct User {
id: i32,
name: String,
}
impl PartialOrd for User {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.name.cmp(&other.name))
}
}
fn main() {
let a = User {
id: 1,
name: "a".to_owned(),
};
let b = User {
id: 2,
name: "b".to_owned(),
};
if a < b {
println!("a < b");
}
}
- .cmp() is made available through #[derive(Ord)]
- Automatically derived on primitive types
Recap
- Structs can be sorted and compared
- PartialOrd and PartialEq implementation required
- Ord implementation optional
- These traits can be used with derive
- Ordering respects only the first struct field when derived
- Manual implementation required to order on different fields
- Ordering and equality must remain consistent when implementing manually
Stdlib Operator Overloading
Operator Overloading
- Operators can be overloaded for structs and enums
- Trait implementation required
- All overloadable operators are available in std::ops mudule
- Behavior should be consistent with the meaning of the operator
- Adding should make something larger
- Substracting should make it smaller, etc
Example Add
use std::ops::Add;
struct Speed(u32);
impl Add<Self> for Speed {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Speed(self.0 + rhs.0)
}
}
fn main() {
let fast = Speed(5) + Speed(3);
}
Example Add w/Different Output
use std::ops::Add;
struct Letter(char);
impl Add<Self> for Letter {
type Output = String;
fn add(self, rhs: Self) -> Self::Output {
format!("{}{}", self.0, rhs.0)
}
}
fn main() {
println!("{}", Letter('h') + Letter('i'));
}
Common Operators
ops::Add + lhs + rhs
ops::Sub - lhs - rhs
ops::Mul * lhs * rhs
ops::Div / lhs / rhs
ops::Rem % lhs % rhs
ops::Not ! !item
ops::Neg - -item
Index Operator
use std::ops::Add;
use std::ops::Index;
enum Temp {
Current,
Max,
Min,
}
struct Hvac {
current_temp: i16,
max_temp: i16,
min_temp: i16,
}
impl Index<Temp> for Hvac {
type Output = i16;
fn index(&self, index: Temp) -> &Self::Output {
match index {
Temp::Current => &self.current_temp,
Temp::Max => &self.max_temp,
Temp::Min => &self.min_temp,
}
}
}
fn main() {
let env = Hvac {
current_temp: 30,
max_temp: 60,
min_temp: 0,
};
let current = env[Temp::Current];
println!("current={}", current);
}
Recap
- Operators are overloaded via traits
- Listing of traits is in std::ops module
- Input type can be specified with generic parameter
- Output type can be specified with the Output associated type alias
- Behavior should remain consistent with operator purpose
Iterators Implementing Iterator
Iterator
- Iterator is provided by the Iteratortrait
- Only one function to be implemented
- Provides for…in syntax
- Access to all iterator adapters
- map, take, filter, etc
- Can be implemented for any structure
Iterator Trait
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Example Iterator
struct Odd {
number: isize,
max: isize,
}
impl Odd {
fn new(max: isize) -> Self {
Self { number: -1, max }
}
}
impl Iterator for Odd {
type Item = isize;
fn next(&mut self) -> Option<Self::Item> {
self.number += 2;
if self.number <= self.max {
Some(self.number)
} else {
None
}
}
}
fn main() {
let mut odds = Odd::new(7);
println!("{:?}", odds.next()); // Some(1)
println!("{:?}", odds.next()); // Some(3)
println!("{:?}", odds.next()); // Some(5)
println!("{:?}", odds.next()); // Some(7)
println!("{:?}", odds.next()); // None
// for..in
// i 3 5 7
for odd in odds{
println!("odd: {:?}", odd);
}
// adapter
// 2 4 6 8
let mut evens = Odd::new(8);
for e in evens.map(|odd| odd + 1) {
println!("even: {}", e);
}
}
Recap
- Implementing Iterator provides access tp for…in syntax and iterator adapters
- Set the output type using the Item associated type as part of the Iterator trait
- Return Some when data is available and None when data are no more items to iterate
- Data structure must:
- Be mutable
- Have a field to track iteration
Iterators Implementing IntoIterator Using An Existing Collection
Iterator Trait
- By default requires mutable access to structure
- Inconvenient
- Not always possible
- Mutation nut always needed
- Solution:
- Implement IntoIterator trait & call .iter() on inner collection
- Vector, HashMap
- Implement IntoIterator trait & call .iter() on inner collection
IntoIterator Trait
- Yields an Iterator (yield items/values)
- Implementation details determine how items are accessed
- Borrow, mutable, move
- Implementation details determine how items are accessed
IntoIterator Trait
trait IntoIterator {
type Item;
type IntoIter;
fn into_iter(self) -> Self::IntoIter;
}
Move
struct Friends {
names: Vec<String>,
}
impl IntoIterator for Friends {
type Item = String;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.names.into_iter()
}
}
fn main() {
let names = vec!["Albert".to_owned(), "Sara".to_owned()];
let mut friends = Friends { names };
// friends` moved due to this implicit call to `.into_it
for f in friends {
println!("{:?}", f);
}
//! value used here after move
// for f in friends {
// println!("{:?}", f);
// }
}
Borrow
struct Friends {
names: Vec<String>,
}
impl<'a> IntoIterator for &'a Friends {
type Item = &'a String;
type IntoIter = std::slice::Iter<'a, String>;
fn into_iter(self) -> Self::IntoIter {
self.names.iter()
}
}
fn main() {
let names = vec!["Albert".to_owned(), "Sara".to_owned()];
let mut friends = Friends { names };
for f in &friends {
println!("{:?}", f);
}
for f in &friends {
println!("{:?}", f);
}
}
Mutable Borrow
struct Friends {
names: Vec<String>,
}
impl<'a> IntoIterator for &'a mut Friends {
type Item = &'a mut String;
type IntoIter = std::slice::IterMut<'a, String>;
fn into_iter(self) -> Self::IntoIter {
self.names.iter_mut()
}
}
fn main() {
let names = vec!["Albert".to_owned(), "Sara".to_owned()];
let mut friends = Friends { names };
for f in &mut friends {
*f = "Frank".to_string();
println!("{:?}", f);
}
}
Iter Methods
- Convention for exposing iteration is to provide up to two methods:
- .iter()
- Iteration over borrowed values
- .iter_mut()
- Iteration over borrowed mutable values
- .iter()
- Implement these by simple calling into_iter() after implementing the IntoIterator trait
- These are optional, but allow for easy combinator usage without the for loop
Recap
- IntoIterator trait yields iterators
- Allows control over borrows & mutability
- Implementation of IntoIterator requires:
- An Item type -yielded value
- An IntoIter - mutable struct which tracks iteration progress / proxy to data structure
- The IntoIter type can be retrieved from the documention on your inner collection
Demo Implementing IntoIterator
use std::collections::HashMap;
#[derive(Debug, Hash, Eq, PartialEq)]
enum Fruit {
Apple,
Banana,
Orange,
}
struct FruitStand {
fruits: HashMap<Fruit, u32>,
}
impl IntoIterator for FruitStand {
type Item = (Fruit, u32);
type IntoIter = std::collections::hash_map::IntoIter<Fruit, u32>;
fn into_iter(self) -> Self::IntoIter {
self.fruits.into_iter()
}
}
// borrow
impl<'a> IntoIterator for &'a FruitStand {
type Item = (&'a Fruit, &'a u32);
type IntoIter = std::collections::hash_map::Iter<'a, Fruit, u32>;
fn into_iter(self) -> Self::IntoIter {
self.fruits.iter()
}
}
// mut borrow
impl<'a> IntoIterator for &'a mut FruitStand {
type Item = (&'a Fruit, &'a mut u32);
type IntoIter = std::collections::hash_map::IterMut<'a, Fruit, u32>;
fn into_iter(self) -> Self::IntoIter {
self.fruits.iter_mut()
}
}
fn main() {
let mut fruits = HashMap::new();
fruits.insert(Fruit::Banana, 5);
fruits.insert(Fruit::Apple, 2);
fruits.insert(Fruit::Orange, 6);
let store = FruitStand{fruits};
for (fruit, stock) in store.into_iter() {
println!("{:?} {:?}", fruit, stock);
}
}
Iterator Implementing IntoIterator Using a Custom Iterator
Mini Iterator Review
- Iterator trait allows iteration over a collection
- Yield items
- Struct must be mutable & contain iteration state information
- IntoIterator trait defines a proxy struct & determines how data is accessed
- Move, borrow, mutation
Problem
- Implementing IntoIterator allows control of the iteration, but …
- We aren’t using an existing collection to store data
- No .iter() or .into_iter()
- We don’t want to pollute our data structure with iteration information
- We aren’t using an existing collection to store data
Solution
- Make an intermediary struct
- Implement Iterator
- Mutable, handles iteration state
- Implement Iterator
- Implement IntoIterator on data struct
- Combined with the intermediary struct will allow iteration
Setup
struct Color {
r: u8,
g: u8,
b: u8,
}
struct ColorIntoIter {
color: Color,
pos: u8,
}
struct ColorIter<'a> {
color: &'a Color,
pos: u8,
}
Review Iterator Trait
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Impl Iterator - Move
/// Impl Iterator - Move
impl Iterator for ColorIntoIter {
type Item = u8;
fn next(&mut self) -> Option<Self::Item> {
let next = match self.pos {
0 => Some(self.color.r),
1 => Some(self.color.g),
2 => Some(self.color.b),
_ => None,
};
self.pos += 1;
next
}
}
/// Impl IntoIterator - Move
impl IntoIterator for Color {
type Item = u8;
type IntoIter = ColorIntoIter;
fn into_iter(self) -> Self::IntoIter {
Self::IntoIter {
color: self,
pos: 0,
}
}
}
Impl Iterator - Borrow
impl<'a> Iterator for ColorIter<'a> {
type Item = u8;
fn next(&mut self) -> Option<Self::Item> {
let next = match self.pos {
0 => Some(self.color.r),
1 => Some(self.color.g),
2 => Some(self.color.b),
_ => None,
};
self.pos += 1;
next
}
}
Impl IntoIterator - Borrow
impl<'a> IntoIterator for &'a Color {
type Item = u8;
type IntoIter = ColorIter<'a>;
fn into_iter(self) -> Self::IntoIter {
Self::IntoIter {
color: &self,
pos: 0,
}
}
}
Notes
- Non-trivial to implement mutable iteration using IntoIterator
- Collect mutable references into a Vector and return it
- Use unsafe to bypasss compiler checks
- Prefer using existing .iter() methods on structures when possible
- Vectors, HashMaps, etc
- Easier to work with, covers most cases
Recap
- Custom iteration requires a dedicated iteration struct for each type of data handling mechanism
- Move, borrow
- Prefer using the .iter() methods on existing collections if possible
stdlib MaCros
assert
let a = 1;
let b = 2;
assert!(a == b, "{} ne {}", a, b);
assert_eq(a, b, "values should be equal");
assert_ne(a, b, "values should not be equal");
dbg
#[derive(Debug)]
enum RoomType {
Bedroom,
Kitchen,
}
#[derive(Debug)]
struct Room {
dimensions: (usize, usize),
kind: RoomType,
}
fn main() {
let kitchen = Room {
dimensions: (20, 20),
kind: RoomType::Kitchen,
};
dbg!(&kitchen);
}
format
let h = "hello";
let w = "World";
let greet: String = format!("{}, {}", h, w);
println!("{}", greet);
include_str
Data file path is relative to the source file
let msg = include_str!("../msg.txt");
println!("{}", msg);
include_bytes
Data is saved as an array of bytes(u8)
let bytes = include_bytes!("image.png");
env
Include string data at compile time, based on environment variable
let config_1 = env!("CONFIG_1");
//error: environment variable `CONFIG_1` not defined
// --> src/main.rs:3:20
// |
// | let config_1 = env!("CONFIG_1");
// | ^^^^^^^^^^^^^^^^
// |
// = note: this error originates in the macro `env` (in Nightly builds, run with -Z macro-backtrace for more info)
todo / unimplemented
- todo!
- Incomplete code sections, with intent to implement
- unimplemented!
- Incomplete code sections, with no intent to implement
- Program will panic when line is executed
- todo!(“taking a vacation”);
- unimplemented!(“nobody wants this”);
unreachable
- Indicates that some code should never be executed
- Useful as both a debugging tool and to ease working with match arms
- Will panic at runtime if the macro is executed
fn main() {
let number = 12;
let max_5 = {
if number > 5 {
5
} else {
number
}
};
match max_5 {
n @ 0..=5 => println!("n = {}", n),
_ => unreachable!("n > 5. this is a bug"),
}
}
Recap
- assert is used to confirm if something is true
- dbg can be used to inspect values while coding
- format provides string interpolation
- include_str & include_bytes copy data from a file into the compiled binary
- env copies an environment variable into the binary
- todo indicates unfinished code
- unimplemented indicates code that will not be finished
- unreachable indicates code that should never execute
stdlib Managing Integer Overvflow
Overflow
- Primitive integers can overflow when they reach their limits
- In debug mode: panic
- In release mode: wrap
- Wrapping may or may not be desired
- Functions exist to handle these sifuations
- Communicate intent
- Reduce bugs
About Functions
- Defined on interger types
- 8, 16, 32, 64, 128 bit signed & unsigned
- Typical operations such as addition, division, multiplication each have functions to handle overflow
- Each function has different behavior when overflow occurs
Overflow Functions
- checked_* Option
- Returns an option
let n: Option<u32> = 0u32.checked_sub(1); // None let n: Option<u32> = u32::MAX.checked_add(1); // None let n: Option<u32> = 9_u32.checked_add(1); // Some(10)
- Returns an option
- overflowing_* (i32, bool)
- Indicates whether overflow occurred
// (4294967295, true) let n:(u32, bool) = 0u32.overflowing_sub(1); // (6, false) let n:(u32, bool) = 5u32.overflowing_add(1);
- Indicates whether overflow occurred
- saturating_*
- Limits the value to the min or max of the type
// 0 let n: u32 = 0_u32.saturating_sub(9001); // 4294967295 let n: u32 = u32::MAX.saturating_add(u32::MAX);
- Limits the value to the min or max of the type
- wrapping_*
- Wraps on overflow (default)
// 4294967295 let n: u32 = 1_u32.wrapping_sub(2); // 0 let n: u32 = u32::MAX.wrapping_add(1);
- Wraps on overflow (default)
Recap
- Arithmetic overflow will panic in debug builds & overflow in release builds
- Overflow functions exist to handle these situations in different ways
- When performing arithmetic on numbers at the extremes, prefer using an overflow function to reduce bugs
Fundamentals Turbofish
What is a turbofish
- Sometimes the compiler cannot determine the type of same data
- A few options are available when this happens:
- Type annotations
- Turbofish
Type Annotations Review
let numbers: Vec<u32> = vec![1, 2, 3];
let numbers: Vec<_> = vec![1, 2, 3];
let odds: Vec<_> = numbers.iter().filter(|n| **n % 2 == 1).collect();
Syntax
ident::<type>
::<>
When Turbofish Can Be Used
- Any item having a generic parameter
pub fn collect<B>(self) -> B
collect::<>()
Recap
- Turbofishs is a way to specify a type when working with generics
- Only needed if the compiler cannot determing the type being used
- Usually optional; type annotations suffices
Fundamentals Loop Labels
Loop Labels
- Loops can be annotated with a label for control flow
- Allows changing flow control to an outer loop
- break
- continue
- Useful when working with nested loops
Syntax
'ident: loop {}
'ident: for x in y {}
'ident: while true {}
/// ident is the name give to the loop
Example break
fn main() {
let matrix = [
[2, 4, 6],
[8, 9, 10],
[12, 14, 16]
];
'rows: for row in matrix.iter() {
'cols: for col in row {
if col % 2 == 1 {
println!("odd: {}", col);
break 'rows;
}
println!("{}", col);
}
}
}
/// 2, 4, 6, 8, odd:9
Example continue
type UserInput<'a> = Result<&'a str, String>;
'menu: loop {
println!("menu");
'input: loop {
let user_input: UserInput = Ok("next");
match user_input {
Ok(input) => break 'menu,
Err(_) => {
println!("try again");
continue 'input;
}
}
}
}
Recap
- Loop labels can be applied to any type of loop
- loop, while, for
- Control can be directed to outer loops using loop labels
- Break will exit the specified loop
- Continue will exectue the specified loop
- 'ident: loop {}
Fundamentals Loop Expresions
- Loops are expresions
- Values can be returned from loops
- Can only be used with loop
- break optionally return a value
Sytax
let value = 5;
let result: usize = 'ident: loop {
break value;
break 'ident value;
};
// result == 5
Example
let value: usize = loop {
if let Ok(input) = get_input() {
match input.parse::<usize>() {
Ok(value) => break value,
Err(e) => continue,
}
}
};
Example Loop Labels
let nums = vec![1, 2, 3, 4, 5, 6, 7, 8];
let div_by_three: Option<usize> = 'outer: loop {
for n in nums {
if n % 3 == 0 {
break 'outer Some(n);
}
}
break None;
};
Recap
- break can return a value to a loop expression
- Only valid on loop (not while or for)
- Can alse break to a loop label with a value
let data = loop { break 'label value; };
Fundamentals Struct Update Syntax
Struct Instantiation
- Structs may have many fields to set during creation
- Lots of code
- Default can be used to set the default values
- Sometimes one or two fields may need to have non-default values
- Possible mutability, lots of boilerplate
- Sometimes one or two fields may need to have non-default values
Setup
struct Particle {
color: (u8, u8, u8),
alpha: u8,
size: (u32, u32),
position: (i32, i32),
velocity: i32,
direction: f32,
}
impl Default for Particle {
fn default() -> Self {
Self {
color: (255, 0, 255),
alpha: 255,
size: (100, 100),
position: (0, 0),
velocity: 0,
direction: 0.0,
}
}
}
Without Struct Update
let mut particle = Particle::default();
particle.alpha = 127;
let particle = particle;
println!("{:#?}", particle);
Struct Update w/Other Struct
let red_particle = Particle {
color: (255, 0, 0),
..Particle::default()
};
let faset_particle = Particle {
velocity: 10,
..red_particle
};
Recap
- Struct update syntax allows structs to be easily instantiated
- Can be used with
- default
- Another struct of the same type
let s = Struct{ field: value,s ..Struct::default() };
Fundamentals Escape Sequences & Raw Strings
Escape Sequences
- Not always possible or convenient to include certain characters in a string
- Quotes, newlines, tabs, Unicode
- Escap sequences allow inclusion of any type of character
Example
let msg = "Hello\nWorld!";
let msg = "Hello\tWorld!";
let msg = "Left\\Right!";
let msg = "Over\"there\"";
let smiley = "\u{1f642}"; // 🙂
Raw Strings
- Escape sequences are disbaled
- Clearer code when multiple special characters are needed
let msg = r"Hello
World";
let msg = r"Hello world";
let msg = r"left\right";
let msg = r#"Over "there""#;
let msg = r##"Over #"#there#"#"##;
let smiley = r"🙂";
Recap
- Escape sequences are a way to include special characters in strings
- Raw strings allow insertion of any characters without using escapes
- Newlines in raw string will include indents
- Surround raw strings with hashes(#) if you need to include double quotes