Ownership & Lifetimes
Ownership Review
- Data in Rust programs have an owner
- Owner os responsible for cleaning up data
- Memory management
- Only one owner (by default)
- Functions, closures, structs, enums, and scopes are owners
- Owner os responsible for cleaning up data
- Data can be transferred (moved) from one owner to another
- Function calls, variable reassignment and closures
- Possible to “borrow” data from an onwer
- Owner still responsible for clean up
Ownership Review - Example
#[derive(Debug)]
enum FrozenItem {
IceCube,
}
#[derive(Debug)]
struct Freezer {
contents: Vec<FrozenItem>,
}
fn place_item(freezer: &mut Freezer, item: FrozenItem) {
freezer.contents.push(item);
}
fn main() {
let mut freezer = Freezer { contents: vec![] };
let cube = FrozenItem::IceCube;
place_item(&mut freezer, cube);
// cube no longer available
}
Lifetimes
- A way to inform the compiler that borrowed data will be valid at a specific point in time
- Needed for
- Storing borrowed data in structs or enums
- Returning borrowed data from functions
- All data has a lifetime
- Most cases are elided
Lifetime Syntax - struct
- Convention use 'a, 'b, 'c
- 'static is reserved
- 'static data stays in memory until the program terminates
struct Name<'a> {
field: &'a DataType,
}
Lifetime Example - struct
enum Part {
Bolt,
Panel,
}
struct RobotArm<'a> {
part:&'a Part,
}
struct AssemblyLine {
parts:Vec<Part>,
}
fn main() {
let line = AssemblyLine { parts: vec![Part::Bolt, Part::Panel]};
{
let arm = RobotArm { part: &line.parts[0]};
};
// arm no longer exists
}
Lifetime Syntax - function
fn name<'a>(arg:&'a DataType) -> &'a DataType {}
Solidifying understanding
- Lifetime annotations indicate that there exists some owned data that:
- Lives at least as long as the borrowed data
- Outlives or outlasts the scope of a borrow
- Exists longer than the scope of a borrow
- Structures utilizing borrowed data must:
- Always be created after the owner was created
- Always be destroyed before the owner is destroyed
Recap
- Lifetimes allow:
- Borrowed data in a structure
- Returning references from functions
- Lifetimes are the mechanisim that tracks how long a piece of data resides in memory
- Lifetimes are usually elided, but can be specified manually
- Lifetimes will be checked by the compiler
Demo Lifetimes
#[derive(Debug)]
struct Cards {
inner: Vec<IdCard>,
}
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
enum City {
Barland,
Bazopolis,
Fooville,
}
#[derive(Debug)]
struct IdCard {
name: String,
age: u8,
city: City,
}
impl IdCard {
pub fn new(name: &str, age: u8, city: City) -> Self {
Self {
name: name.to_string(),
age,
city,
}
}
}
fn new_ids() -> Cards {
Cards {
inner: vec![
IdCard::new("Amy", 1, City::Fooville),
IdCard::new("Matt", 10, City::Barland),
IdCard::new("Bailee", 20, City::Barland),
IdCard::new("Anthony", 30, City::Bazopolis),
IdCard::new("Tina", 40, City::Bazopolis),
],
}
}
#[derive(Debug)]
struct YoungPeople<'a> {
inner: Vec<&'a IdCard>,
}
impl<'a> YoungPeople<'a> {
fn living_in_fooville(&self) -> Self {
Self {
inner: self
.inner
.iter()
.filter(|id| id.city == City::Fooville)
.map(|id| *id)
.collect(),
}
}
}
fn main() {
let ids = new_ids();
let young = YoungPeople {
inner: ids.inner.iter().filter(|id| id.age <= 20).collect(),
};
println!("ids");
for id in ids.inner.iter() {
println!("{:?}", id);
}
println!("\nyoung");
for id in young.inner.iter() {
println!("{:?}", id);
}
}
Activity Lifetimes & Structures
// Topic: Lifetimes & Structures
//
// Requirements
// * Display just the names and titles of persons from the mock-data.csv file
// * The names & titles must be stored in a struct separately from the mock
// data for potential later usage
// * None of the mock data may be duplicated in memory
//
// Notes:
// * The mock data has already been loaded with the include_str! macro, so all funtionality
// must be implemented using references/borrows
// id, first_name, email, dept, title
const MOCK_DATA: &'static str = include_str!("mock-data.csv");
struct Names<'a> {
inner: Vec<&'a str>,
}
struct Titles<'a> {
inner: Vec<&'a str>,
}
fn main() {
let data = MOCK_DATA.split('\n').skip(1).collect();
let names = data
.iter()
.filter_map(|line| line.split(',').nth(1))
.collect();
let names = Names { inner: names };
let titles = data
.iter()
.filter_map(|line| line.split(',').nth(4))
.collect();
let titles = Titles { inner: titles };
let data = names.inner.iter().zip(titles.inner.iter());
for (name, title) in data.take(5) {
println!("name: {}, title: {}", name, title);
}
}
Activity Lifetimes & Function
fn longest<'a>(one:&'a str, two:&'a str) ->&'a str {
if two > one {
return two;
}else {
return one;
}
}
fn main(){
let short = "hello";
let long = "this is a long message";
println!("{}",longest(short, long));
}
Custom Errors
- Functions may fail in more than one way
- Useful to communicate the failure reason
- Error enumeration
- Enumertations allow errors to be easily defined
- Can match on the enumeration to handle specific error conditions
Error Requirements
- Implement the Debug trait
- Display error info in debug contexts
- Implement the Dispaly trait
- Display error info in user contexts
- Implement the Error trait
- Interop with code using dynamic errors
Manual Error Creation
- Interop with code using dynamic errors
#[derive(Debug)]
enum LockError{
MechanicalError(i32),
NetworkError,
NotAuthorized,
}
use std::error::Error;
impl Error for LockError {}
use std::fmt;
impl fmt::Display for LockError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result{
match self{
self::MechanicalError(code) => write!(f, "Mechanical Error: {}", code),
self::NetworkError => write!(f, "Network Error"),
self::NotAuthorized => write!(f, "Not Authorized"),
}
}
}
The ‘thiserror’ Crate
# Cargo.toml
[dependencies]
thiserror = "1.0"
use thiserror::Error;
#[derive(Debug, Error)]
enum LockError{
#[error("Mechanical Error:{0}")]
MechanicalError(i32),
#[error("Network Error")]
NetworkError,
#[error("Not Authorized")]
NotAuthorized,
}
Usage
fn lock_door() -> Result<(), LockError> {
// ... some code ...
Err(LockError::NetworkError)
}
Error Conversion
use thiserror::Error;
#[derive(Debug, Error)]
enum NetworkError {
#[error("Connection time out")]
Timeout,
#[error("Unreachable")]
Unreachable
}
enum LockError{
#[error("Mechanical Error:{0}")]
MechanicalError(i32, i32),
#[error("Network Error")]
Network(#[from] NetworkError),
#[error("Not Authorized")]
NotAuthorized,
}
Pro Tips: Do’s
- Prefer to use error enumerations over strings
- More concisely commmunicates the problem
- Can be used with match
- Strings are Ok when prototyping, or if the problem domain isn’s fully understood
- Change to enumerations as soon as possible
- Keep errors specific
- Limit error enumerations to:
- Single modules
- Single functions
- Limit error enumerations to:
- Try to use match as much as possible
More Pro Tips: Don’ts - Don’t put unrelated errors into a single enumeration
- As the problem domain expands, the enumeration will become unwiedy
- Changes to the enumeration will cascade across the entire codebase
- Unclear witch errors can be generated by a function
Recap
- Custom error enumerations communicate exactly what wrong in a function
- Errors require three trait implementations
- Debug (can be derived)
- std::error::Error (empty impl ok)
- Display (manual or crate)
- Use the thiserror crate to easily implement all required traits for errors
- Keep error enumerations module or function specific
- Don’t put too many variants in one error
Demo Custom Errors
use chrono::{DateTime, Duration, Utc};
use thiserror::Error;
struct SubwayPass {
id: usize,
funds: isize,
expires: DateTime<Utc>,
}
#[derive(Error, Debug)]
enum PassError {
#[error("data store disconnected")]
PassExpired,
#[error("insufficient funds: {0}")]
InfufficientFunds(isize),
#[error("pass read error: {0}")]
ReadError(String),
}
fn swipe_card() -> Result<SubwayPass, PassError> {
Ok(SubwayPass {
id: 0,
funds: 200,
expires: Utc::now() + Duration::weeks(52),
})
// Err(PassError::ReadError("Magstrip failed to read message".to_owned()))
}
fn use_pass(pass: &mut SubwayPass, cost: isize) -> Result<(), PassError> {
if Utc::now() > pass.expires {
Err(PassError::PassExpired)
} else {
if pass.funds - cost < 0 {
Err(PassError::InfufficientFunds(pass.funds))
} else {
pass.funds = pass.funds - cost;
Ok(())
}
}
}
fn main() {
let pass_status = swipe_card().and_then(|mut pass| use_pass(&mut pass, 3));
match pass_status {
Ok(_) => println!("ok to board"),
Err(e) => match e {
PassError::ReadError(s) => (),
PassError::PassExpired => (),
PassError::InfufficientFunds(f) => (),
},
}
}
Demo const
const MAX_SPEED: i32 = 9000;
fn clamp_speed(speed: i32) -> i32 {
if speed > MAX_SPEED {
9000
} else {
speed
}
}
fn main() {}
Demo New Types
#[derive(Debug, Copy, Clone)]
struct NeverZero(i32);
impl NeverZero {
fn new(x: i32) -> Result<Self, String> {
if i == 0 {
Err("cannot be zero".to_owned())
} else {
Ok(Self(i))
}
}
}
fn divide(a: i32, b: NeverZero) -> i32 {
let b = b.0;
a / b
}
fn main() {
match NeverZero::new(5) {
Ok(nz)=> println!("{:?}", divide(10, nz)),
Err(e) => println!("{:?}",e),
}
}
Activity New Types
#[derive(Debug)]
enum Color {
Black,
Blue,
Brown,
Custom(String),
Gray,
Green,
Purple,
Red,
White,
Yellow,
}
/// Each new type should implement a 'new' function
#[derive(Debug)]
struct ShirtColor(Color);
impl ShirtColor {
fn new(color: Color) -> Self {
Self(color)
}
}
#[derive(Debug)]
struct ShoesColor(Color);
impl ShoesColor {
fn new(color: Color) -> Self {
Self(color)
}
}
#[derive(Debug)]
struct PantsColor(Color);
impl PantsColor {
fn new(color: Color) -> Self {
Self(color)
}
}
fn print_shirt_color(color: ShirtColor) {
println!("shirt color = {:?}", color);
}
fn print_shoes_color(color: ShoesColor) {
println!("shoes color = {:?}", color);
}
fn print_pants_color(color: PantsColor) {
println!("pants color = {:?}", color);
}
fn main() {
let shirt_color = ShirtColor::new(Color::Gray);
let shoes_color = ShoesColor::new(Color::Blue);
let pants_color = PantsColor::new(Color::White);
print_shirt_color(shirt_color);
print_shoes_color(shoes_color);
print_pants_color(pants_color);
}
Typesstate Pattern
**Typestates
- Leverage type system to encode state changes
- Implemented by creating a type for each state
- Use move semantics to invalidate a state
- Return next state from previous state
- Optionally drop the state
- Close file, connection dropped, etc
- Complie time enforcement of logic
struct BusTicket;
struct BoardedBusTicket;
impl BusTicket {
fn board(self) -> BoardedBusTicket {
BoardedBusTicket
}
}
fn main() {
let ticket = BusTicket;
let board = ticket.board();
// Complie error
// use of moved value: `ticket`
ticket.board();
}
struct File<'a>(&'a str);
impl<'a> File<'a> {
fn read_bytes(&self) -> Vec<u8> {
// read data
let v: Vec<u8> = vec![];
v
}
fn delete(self) {
// delete file
}
}
fn main() {
let file = File("data.txt");
let data = file.read_bytes();
file.delete();
//Compile error
let read_again = file.read_bytes();
}
Recap
- Typestates leverage the compiler to enforce logic
- Can be used for:
- Invalidating / consuming states
- Properly transsitioning to another state
- Disallowing access to a missing resource
Demo Typestate Pattern
struct Employee<State> {
name: String,
state: State,
}
impl<State> Employee<State> {
fn transition<NextState>(self, state: NextState) -> Employee<NextState> {
Employee {
name: self.name,
state: state,
}
}
}
struct Agreement;
struct Signature;
struct Training;
struct FailedTraining {
score: u8,
}
struct OnBoardingComplete {
score: u8,
}
impl Employee<Agreement> {
fn new(name: &str) -> Self {
Self {
name: name.to_string(),
state: Agreement,
}
}
fn read_agreement(self) -> Employee<Signature> {
self.transition(Signature)
}
}
impl Employee<Signature> {
fn sign(self) -> Employee<Training> {
self.transition(Training)
}
}
#[rustfmt::skip]
impl Employee<Training> {
fn train(self, score: u8) -> Result<Employee<OnBoardingComplete>, Employee<FailedTraining>> {
if score >= 7 {
Ok(self.transition(OnBoardingComplete { score }))
} else {
Err(self.transition(FailedTraining { score }))
}
}
}
fn main() {
let employee = Employee::new("Sara");
let onboarded = employee.read_agreement().sign().train(6);
match onboarded {
Ok(completed) => println!("Completed"),
Err(emp) => println!("training failed, score:{}", emp.state.score),
}
}
Activity Typestate Pattern
#[derive(Copy, Debug, Clone)]
struct LuggageId(usize);
struct Luggage(LuggageId);
struct CheckIn(LuggageId);
struct OnLoad(LuggageId);
struct OffLoad(LuggageId);
struct AwaitingPickup(LuggageId);
struct EndCustody(LuggageId);
impl Luggage {
fn new(id: LuggageId) -> Self {
Luggage(id)
}
fn check_in(self) -> CheckIn {
CheckIn(self.0)
}
}
impl CheckIn {
fn onload(self) -> OnLoad {
OnLoad(self.0)
}
}
impl OnLoad {
fn offload(self) -> OffLoad {
OffLoad(self.0)
}
}
impl OffLoad {
fn carousel(self) -> AwaitingPickup {
AwaitingPickup(self.0)
}
}
impl AwaitingPickup {
fn pickup(self) -> (Luggage, EndCustody) {
(Luggage(self.0), EndCustody(self.0))
}
}
fn main() {
let id = LuggageId(1);
let luggage = Luggage::new(id);
let luggage = luggage.check_in().onload().offload().carousel();
let (luggage, _) = luggage.pickup();
println!("luggage {:?}", luggage.0);
}
enum Species {
Finch,
Hawk,
Parrot,
}
struct Bird {
age:usize,
species:Species,
}
#[rustfmt::ship]
fn main() {
let hawk = Bird{
age: 13,
species:Species::Hawk,
};
match hawk {
Bird{age:4, ..} => println!("4 years old bird"),
Bird{age:4..=10| 15..=20, ..} => println!("4~10 or 15~20 years old bird"),
Bird{species:Species::Finch, ..} => println!("finch!"),
Bird{..}=> println!("other bird"),
}
}
Demo Match Guards & Binding
enum
enum Status {
Error(i32),
Info,
Warn,
}
fn main() {
let status = Status::Error(8);
match status {
/// @ symbols this is called bingding
/// bingding some variable to the value
/// bind 3 to s
Status::Error(s @ 3) => println!("error three"),
///
Status::Error(s @ 5..=6) => println!("error 5 or 6:{}", s),
Status::Error(s @ 4..=10) => println!("error three througt ten:{}", s),
Status::Error(s @ 18 | s @ 19) => println!("error 18 or 19"),
Status::Error(s) => println!("error code:{}", s),
Status::Info => println!("info"),
Status::Warn => println!("warn"),
}
}
struct
struct Vehicle {
km: usize,
year: usize,
}
#[rustfmt::ship]
fn main() {
let car = Vehicle {
km: 80_000,
year: 2020,
};
match car {
Vehicle { km, year } if km == 0 && year == 2020 => println!("new 2020 car"),
Vehicle { km, .. } if km <= 50_000 => println!("under 50k km"),
Vehicle { km, .. } if km >= 80_000 => println!("at least 80k km"),
Vehicle { year, .. } if year == 2020 => println!("made in 2020"),
Vehicle { .. } => println!("other mileage"),
}
}
Activity Match Guards & Binding
#[derive(Debug)]
enum TreasureItem {
Gold,
SuperPower,
}
#[derive(Debug)]
struct TreasureChest {
content: TreasureItem,
amount: usize,
}
#[derive(Debug)]
struct Pressure(u16);
#[derive(Debug)]
enum BrickStyle {
Dungenon,
Gray,
Red,
}
#[derive(Debug)]
enum Tile {
Brick(BrickStyle),
Dirt,
Grass,
Sand,
Treasure(TreasureChest),
Water(Pressure),
Wood,
}
fn print_tile(tile: Tile) {
use Tile::*;
match tile {
Brick(brick @ BrickStyle::Red | brick @ BrickStyle::Gray) => {
println!("the brick color is {:?}", brick)
}
Brick(other) => println!("{:?} brick", other),
Dirt | Grass | Sand => println!("Ground tile"),
Treasure(TreasureChest { amount, .. }) if amount >= 100 => println!("lots of gold"),
Water(pressure) if pressure.0 < 10 => println!("water pressure leavel:{:?}", pressure.0),
Water(pressure) if pressure.0 >= 10 => println!("High water pressure"),
_ => (),
}
}
fn main() {
let tile = Tile::Brick(BrickStyle::Red);
print_tile(tile);
let tile = Tile::Sand;
print_tile(tile);
let tile = Tile::Treasure(TreasureChest {
content: TreasureItem::Gold,
amount: 200,
});
print_tile(tile);
let tile = Tile::Water(Pressure(9));
print_tile(tile);
}
Slices Arrays & Slices
Arrays
- Contiguous memory region
- All elements have the same size
- Arrays are not dynamically sized
- Size must be hard-coded
- Usually prefer Vector
- Useful when writing algorithms with a fixed buffer size
- Networkong, cropto, matrices
Syntax
- Networkong, cropto, matrices
let numbers = [1,2,3,4];
/// [type; element count]
let numbers: [u8; 3] = [1,2,3];
Slices
- A borrowed view into an array
- Can be iterated upon
- Optionally mutable
- Indices bounded by the slice size
- Cannot go out of bounds of the initial slice
- Can create any number of subslices from an existing slice
Slices - View Into An Array
[char; 10]
Array 0 1 2 3 4 5 6 7 8 9
A B C D E F G H I J
Slice 0 1 2 3
&[char]
Slices & Vectors
- Borrowing a Vector as an argument to a function that requires a slice will automatically obtain a slice
- Always prefer to borrow a slice instead of a Vector
fn func(slice: &[u8]) {}
let numbers = vec![1, 2, 3];
func(&numbers);
let numbers: &[u8] = numbers.as_slice();
Slicing With Ranges
let chars = vec!['A', 'B', 'C', 'D'];
let bc = &chars[1..=2];
let ab = &chars[0..2];
Vector Vector
0 1 2 3 0 1 2 3
A B C D A B C D
0 1 0 1
Slice Slice
Subslices
let chars = vec!['A', 'B', 'C', 'D'];
let bcd = &chars[1..=3];
let cd = &bcd[1..=2];
Vector Slice
0 1 2 3 0 1 2
A B C D --> B C D
0 1 2 0 1
Slice Slice
Recap
- Array must be statically initialized with hard-coded lengths
- Slices are a way to access parts of an array
- Array-backed data structures like Vectors can be sliced
- Slice lengths are always bound by the size of the slice
- Subslices can be created from existing slices
Slice Patterns
Use Case
- Read the first few bytes to determine header information
- Take different actions based on the data using match
- Get the first or last elements of a slice
- No need for bounds checking on slices
- Compiler ensures access are always within bounds
Example
- Compiler ensures access are always within bounds
let chars = vec!['A', 'B', 'C', 'D'];
match chars.as_slice() {
// first take the first element of the slice
// last take the last element of the slice
// .. ignore everything between the first and the last
[first, .., last] => (),
// match one element
[single] => (),
// match an empty slice
[] => (),
}
let chars = vec!['A', 'B', 'C', 'D'];
match chars.as_slice() {
[one, two, ..] => (),
[.., last] => (),
[] => (),
}
Overlapping Patterns
- Patterns easily overlap
- Minimize number of match arms to avoid bugs
// second arm always ignored
match slice {
[first, ..] => (),
[.., last] => (),
[] => (),
}
Prevent Overlapping Patterns
- Match the largest patterns first, followed by smaller patters
match slice { slice {
[] => (), [a, b, c, d, .., ] => (),
[a, ..] => (), [a, b, c, .., ] => (),
[a, b, .., ] => (), [a, b, .., ] => (),
[a, b, c, .., ] => (), [a, ..] => (),
[a, b, c, d, .., ] => (), [] => (),
} }
first two arms cover all cases, all arms can be matched
remaining will be ignored
Guars
let nums = vec![7, 8, 9];
match nums.as_slice() {
[first @ 1..=3, rest @ ..] =>{
}
[single] if single == &5 || single ==&6 => ()
[a, b] => (),
[..]=>(),
[]=>(),
}
Recap
- Slices can be matched on specific patterns
- These patterns can include match guards
- Match on largest patterns first, followed by smaller patterns
- Smaller patterns tent to be greedy
- Minimize the number of match arms to avoid bugs
Activity Slices
fn data() -> &'static [u64] {
&[5, 5, 4, 4, 3, 3, 1]
}
fn process_chunk(data: &[u64]) {
match data {
[lhs, rhs] => println!("{} + {} = {}", lhs, rhs, (lhs + rhs)),
[single] => println!("Unpaired value:{}", single),
[] => println!("Data stream complete"),
[..] => unreachable!("chunk size should be at most 2"),
}
}
fn main() {
// 'stream' is an iterator of Option<&[u64]>
let mut stream = data().chunks(2);
for chunk in stream {
process_chunk(chunk);
}
}
Type Aliases
- Give a new name to an existing type
- Basic text substitution
- Simplifies complicated types
- Makes code easier to read & write
- Multiple aliases for the same type will work together, but masybe not as intended
Syntax
type Name = Type;
Example
type ContactName = String;
type Miles = u64;
type Centimeters = u64;
type Callback = HashMap<String, Box<Fn(i32, i32) -> i32>>;
Usage
struct Contact {
name: String,
phone: String,
}
type ContactName = String;
type ContactIndex = HashMap<ContactName, Contact>;
fn add_contact(index: & mut ContactIndex, contact: Contact) {
index.insert(contact.phone.to_owned(), contact);
}
Generics/Lifetimes
type BorrowedItems<'a> = Vec<&'a str>;
type GenericThings<T> = Vec<Thing<T>>;