External Modules
- Allows code to be compartmentalized
- Organized source code management
- Better collaboration
- More intuitive coding
- Quickly identify where imported code is used
Module Details
- Can have any name
- Hierarchical organization
- Private by default
- Use pub keyword to make a module public
- External modules can be a:
- Directory
- Must contain mod.rs
- Can contain additional modules
- File
- Directory
File Structure
Cargo.toml
[lib]
name = "demo"
path = "src/lib/mod.rs"
.
├── bin
| └── app.rs
└── lib
├── codec
│ ├── audio
| | ├── flac.rs
| | ├── mod.rs
| | └── mp3.rs
│ ├── video
| | ├── h264.rs
| | ├── mod.rs
| | └── vp9.rs
│ └── mod.rs
├── mod.rs
└── transcode.rs
Module Declaration
.
├── bin
| └── app.rs
└── lib
├── codec
│ ├── audio
| | ├── flac.rs
| | ├── mod.rs -----------> pub mod flac;
| | | pub mod mp3;
| | └── mp3.rs
│ ├── video
| | ├── h264.rs
| | ├── mod.rs -----------> pub mod h264;
| | | pub mod vp9;
| | └── vp9.rs
│ └── mod.rs -----------> pub mod audio;
| pub mod video;
|
├── mod.rs -----------> pub mod codec;
| pub mod transcode;
└── transcode.rs
the transcode module exists as a file
the codec module exist as a directory
Recap
- Modules are organized hierarchically
- Use super to go up one level
- Use crate to start from the top
- The as keyword can be used to create an alias for a module
- The mod keyword is used to declare a module
- No curly braces for external modules
- Modules can be re-exported with the use keyword
- pub indicates the module may be accessed from anywhere
- Ommitted pub restricts access to only the containing module and sub-modules
Activity External Modules
- code structure
. ├── Cargo.lock ├── Cargo.toml ├── README.md ├── src │ ├── activitylib.rs │ ├── main.rs │ ├── math.rs │ └── msg.rs └── target
- Cargo.toml
[lib] name = "activity" path = "src/activitylib.rs"
- source code
// src/activitylib.rs pub mod msg; pub mod math; // src/msg.rs pub fn trim(msg: &str) -> &str { msg.trim() } pub fn capitalize(msg: &str) -> std::borrow::Cow<'_, str> { if let Some(letter) = msg.get(0..1) { format!("{}{}", letter.to_uppercase(), &msg[1..msg.len()]).into() } else { msg.into() } } pub fn exciting(msg: &str) -> String { format!("{}", msg) } // src/math.rs pub fn add(lhs: isize, rhs: isize) -> isize { lhs + rhs } pub fn sub(lhs: isize, rhs: isize) -> isize { lhs - rhs } pub fn mul(lhs: isize, rhs: isize) -> isize { lhs * rhs } // main.rs fn main() { use activity::math; let result = { let two_plus_two = math::add(2, 2); let three = math::sub(two_plus_two, 1); math::mul(three, three) }; assert_eq!(result, 9); println!("(2 + 2 - 1) * 3 = {}", result); { use activity::msg::{capitalize, exciting, trim}; let hello = { let msg = "hello"; let msg = trim(msg); capitalize(msg) }; let world = { let msg = "world"; exciting(msg) }; let msg = format!("{}, {}", hello, world); assert_eq!(&msg, "Hello, world"); println!("{}", msg); } }
Demo User Input
use std::io;
fn get_input() -> io::Result<String> {
let mut buffer = String::new();
io::stdin().read_line(&mut buffer)?;
Ok(buffer.trim().to_owned())
}
fn main() {
let mut all_input = vec![];
let mut time_input = 0;
while time_input < 2 {
match get_input() {
Ok(words) =>{
all_input.push(words);
time_input += 1;
}
Err(e) =>{
println!("error: {:?}", e);
}
}
}
for input in all_input {
println!("{:?}", input);
}
}
Activity User Input
use std::io;
enum PowerState {
Off,
Sleep,
Reboot,
Shutdown,
Hibernate
}
impl PowerState {
fn new(state: &str)->Option<PowerState> {
let state = state.trim().to_lowercase();
// String -> &str
match state.as_str() {
"off" => Some(PowerState::Off),
"sleep" => Some(PowerState::Sleep),
"reboot" => Some(PowerState::Reboot),
"shutdown" => Some(PowerState::Shutdown),
"hibernate" => Some(PowerState::Hibernate),
_ => None,
}
}
}
fn print_power_action(state:PowerState) {
use PowerState::*;
match state {
Off=> println!("turning off"),
Sleep => println!("sleeping"),
Reboot => println!("rebooting"),
Shutdown => println!("shutting down"),
Hibernate => println!("hibernating"),
}
}
fn main() {
println!("Enter new power state");
let mut buffer = String::new();
let user_input_status = io::stdin().read_line(&mut buffer);
if user_input_status.is_ok() {
match PowerState::new(&buffer) {
Some(state)=> print_power_action(state),
None=> println!("invalid power state"),
}
} else {
println!("error reading input");
}
}
Project Interactive billing application
About
- Command line application to track bills / expenditures
- Add, edit, view, remove
- Purposefully simple
- Focus on working with Result:
- enums, Option, Result, match, iterators, etc
- Ownership / Borrowing issues
- Mutability
- Focus on working with Result:
- Objectives
- Implement previously covered materials in a larger context
- Solidify concepts before moving to more advanced material
Sample
- Solidify concepts before moving to more advanced material
== Manage Bills == 1. Add Bill 2. View Bills 3. Remove Bill 4. Update Bill Enter selection:
- Implement previously covered materials in a larger context
Project
use std::collections::HashMap;
#[derive(Clone, Debug)]
pub struct Bill {
name: String,
amount: f64,
}
pub struct Bills {
inner: HashMap<String, Bill>,
}
impl Bills {
fn new() -> Self {
Self {
inner: HashMap::new(),
}
}
fn add(&mut self, bill: Bill) {
self.inner.insert(bill.name.to_string(), bill);
}
fn get_all(&self) -> Vec<&Bill> {
self.inner.values().collect()
}
fn remove(&mut self, name: &str) -> bool {
self.inner.remove(name).is_some()
}
fn update(&mut self, name: &str, amount: f64) -> bool {
match self.inner.get_mut(name) {
Some(bill) => {
bill.amount = amount;
true
}
None => false,
}
}
}
fn get_input() -> Option<String> {
let mut buffer = String::new();
while std::io::stdin().read_line(&mut buffer).is_err() {
println!("Please enter your data again");
}
let input = buffer.trim().to_owned();
if &input == "" {
None
} else {
Some(input)
}
}
fn get_bill_amount() -> Option<f64> {
println!("Amount");
loop {
let input = match get_input() {
Some(input) => input,
None => return None,
};
if &input == "" {
return None;
}
let parsed_input: Result<f64, _> = input.parse();
match parsed_input {
Ok(amount) => return Some(amount),
Err(_) => println!("Please enter a number"),
}
}
}
mod menu {
use crate::{get_bill_amount, get_input, Bill, Bills};
pub fn add_bill(bills: &mut Bills) {
println!("Bill name:");
let name = match get_input() {
Some(input) => input,
None => return,
};
let amount = match get_bill_amount() {
Some(amount) => amount,
None => return,
};
let bill = Bill { name, amount };
bills.add(bill);
println!("Bill added");
}
pub fn remove_bill(bills: &mut Bills) {
for bill in bills.get_all() {
println!("{:?}", bill);
}
println!("Enter remove bill name: ");
let name = match get_input() {
Some(name) => name,
None => return,
};
if bills.remove(&name) {
println!("Bill removed");
} else {
println!("bill not found");
}
}
pub fn update_bill(bills: &mut Bills) {
for bill in bills.get_all() {
println!("{:?}", bill);
}
println!("Enter update bill name: ");
let name = match get_input() {
Some(name) => name,
None => return,
};
println!("Enter update bill amount: ");
let amount = match get_bill_amount() {
Some(amount) => amount,
None => return,
};
if bills.update(&name, amount) {
println!("Update bill done");
} else {
println!("bill not found");
}
}
pub fn view_bills(bills: &Bills) {
for bill in bills.get_all() {
println!("{:?}", bill);
}
}
}
enum MainMenu {
AddBill,
ViewBill,
RemoveBill,
UpdateBill,
}
impl MainMenu {
fn from_str(input: &str) -> Option<MainMenu> {
match input {
"1" => Some(Self::AddBill),
"2" => Some(Self::ViewBill),
"3" => Some(Self::RemoveBill),
"4" => Some(Self::UpdateBill),
_ => None,
}
}
fn show() {
println!("");
println!("== Manage Bills ==");
println!("1. Add Bill");
println!("2. View Bills");
println!("3. Remove Bill");
println!("4. Update Bill");
println!("");
println!("Enter selection: ");
}
}
fn run_program() -> Option<()> {
// Create bill structure
let mut bills = Bills::new();
loop {
// Display the menu
MainMenu::show();
let input = get_input()?;
match MainMenu::from_str(input.as_str()) {
Some(MainMenu::AddBill) => menu::add_bill(&mut bills),
Some(MainMenu::ViewBill) => menu::view_bills(&bills),
Some(MainMenu::RemoveBill) => menu::remove_bill(&mut bills),
Some(MainMenu::UpdateBill) => menu::update_bill(&mut bills),
None => break,
}
// Make a choice, based on input
}
None
}
fn main() {
run_program();
}
Traits
- A way to specify that some functionality exists
- Used to standardize functionality acrosss multiple different types
- Standardization permits functions to operate on multiple different types
- Code deduplication
Example
trait Noise {
fn make_noise(&self);
}
struct Cat;
impl Noise for Cat {
fn make_noise(&self) {
println!("miao miao");
}
}
struct Dog;
impl Noise for Dog {
fn make_noise(&self) {
println!("wang wang");
}
}
fn hello(noisy: impl Noise) {
noisy.make_noise();
}
fn main() {
hello(Cat{});
hello(Dog{});
}
Recap
- Traits define similar functionality for different types
- Trait functions are just regular functions
- Can accept arguments and return values
- Use impl Trait as a function argument to pass data via trait
Activity Trait
trait Perimeter {
fn calculate_perimeter(&self) -> i32;
}
struct Square {
side: i32,
}
impl Perimeter for Square {
fn calculate_perimeter(&self) -> i32 {
self.side * 4
}
}
struct Triangle {
side_a: i32,
side_b: i32,
side_c: i32,
}
impl Perimeter for Triangle {
fn calculate_perimeter(&self) -> i32 {
self.side_a + self.side_b + self.side_c
}
}
fn print_perimeter(shape: impl Perimeter) {
let perimeter = shape.calculate_perimeter();
println!("perimeter = {:?}", perimeter);
}
fn main() {
let square = Square { side: 5 };
let triangle = Triangle {
side_a: 2,
side_b: 3,
side_c: 4,
};
print_perimeter(square);
print_perimeter(triangle);
}
** Default Trait**
The default trait is used to create new structures and enumerations with a default value
#[derive(Debug)]
struct Package {
weight: f64,
}
impl Package {
fn new(weight: f64) -> Self {
Self { weight }
}
}
// default trait
impl Default for Package {
fn default() -> Self {
Self { weight: 3.0 }
}
}
fn main() {
let p = Package::default();
println!("{:?}", p);
}
Shared Functionality Generic Functions
What Are Generic Functions?
- A way to write a function that can have a single parameter with multiple data types
- Trait is used as function parameter instead of data type
- Function depends on existence of functions declared by trait
- Less code to write
- Automatically works when new data types are intoduced
Quick Review: Traits
trait Move {
fn move_to(&self, x: i32, y: i32);
}
struct Snake;
impl Move for Snake {
fn move_to(&self, x: i32, y: i32) {
println!("slither to ({}, {})", x, y);
}
}
struct Grasshopper;
impl Move for Grasshopper {
fn move_to(&self, x: i32, y: i32) {
println!("hop to ({}, {})", x, y);
}
}
// thing has to implement the move trait
fn make_move(thing: impl Move, x: i32, y: i32) {
thing.move_to(x, y);
}
fn main() {
let python = Snake {};
make_move(python, 1, 1);
}
Generic Syntax
fn function(param1: impl Trait1, param2: impl Trait2) {
/* body */
}
fn function<T: Traits, U: Trait2>(param1: T, param2: U) {
/* body */
}
fn function<T, U>(param1: T, param2: U)
where
T: Trait1 + Trait2,
U: Trait1 + Trait2 + Trait3,
{
/* body */
}
Generic Example
fn make_move(thing: impl Move, x: i32, y: i32) {
thing.move_to(x, y);
}
fn make_move<T: Move>(thing: T, x: i32, y: i32) {
thing.move_to(x, y);
}
fn make_move<T>(thing: T, x: i32, y: i32)
where
T: Move,
{
thing.move_to(x, y);
}
Recap
- Generics let you write one function to work with multiple types of data
- Generic functions are “bound” or “constrained” by the traits
- Only able to work with data that implements the trait
- Three syntaxes available:
- fn func(param: impl Trait) {}
- fn func<T: Trait>(param: T) {}
- fn func(param: T) where T: Trait {}
Demo Generic Functions
trait CheckIn {
fn check_in(&self);
fn process(&self);
}
struct Pilot;
impl CheckIn for Pilot {
fn check_in(&self) {
println!("checked in as pilot");
}
fn process(&self) {
println!("pilot enters the cockpit");
}
}
struct Passenger;
impl CheckIn for Passenger {
fn check_in(&self) {
println!("checked in as passenger");
}
fn process(&self) {
println!("passenger takes a seat");
}
}
struct Cargo;
impl CheckIn for Cargo {
fn check_in(&self) {
println!("cargo checked in");
}
fn process(&self) {
println!("cargo moved to storage");
}
}
fn process_item<T: CheckIn>(item: T) {
item.check_in();
item.process();
}
fn main() {
let paul = Passenger;
let kathy = Pilot;
let cargo1 = Cargo;
let cargo2 = Cargo;
process_item(paul);
process_item(kathy);
process_item(cargo1);
process_item(cargo2);
}
Activity Generic Functions
#[derive(Debug)]
enum ServicePriority {
High,
Standard,
}
trait Priority {
fn get_priority(&self) -> ServicePriority;
}
#[derive(Debug)]
struct ImportantGuest;
impl Priority for ImportantGuest {
fn get_priority(&self) -> ServicePriority {
ServicePriority::High
}
}
#[derive(Debug)]
struct Guest;
impl Priority for Guest {
fn get_priority(&self) -> ServicePriority {
ServicePriority::Standard
}
}
fn print_guest_priority<T: Priority + std::fmt::Debug>(guest: T) {
println!("{:?} is {:?} priority", guest, guest.get_priority());
}
fn main() {
let guest = Guest;
let vip = ImportantGuest;
print_guest_priority(guest);
print_guest_priority(vip);
}
Generic Structures
- Store data of any type within a structure
- Trait bounds restrict the type of data the structure can utilize
- Also known as “generic constraints”
- Trait bounds restrict the type of data the structure can utilize
- Useful when making your own data collections
- Reduces technical debt as program expands
- New data types can utilize generic structures and be easily integrated into the program
Syntax
struct Name<T: Trait1 + Trait2, U: Trait3> {
field1: T,
field2: U,
}
struct Name<T, U>
where
T: Trait1 + Trait2,
U: Trait3,
{
field1: T,
field2: U,
}
Generic Structures impl blocks
Implementing Functionality
- Generic implementation
- Implements functionality for any type that can be used with the structure
- Concrete implementation
- Implements functionality for only the type specified
Concrete Implementation - Setup
trait Game {
fn name(&self) -> String;
}
enum BoardGame {
Chess,
Monopoly,
}
enum VideoGame {
PlayStation,
Xbox,
}
impl Game for BoardGame {
// ...
}
impl Game for VideoGame {
// ...
}
struct PlayRoom<T: Game> {
game: T,
}
impl PlayRoom<BoardGame> {
pub fn cleanup(&mut self) {
// ...
}
}
fn main() {
let video_room = PlayRoom {
game: VideoGame::Xbox,
};
let board_room = PlayRoom {
game: BoardGame::Monopoly,
};
board_room.cleanup();
video_room.cleanup();// method not found in PlayRoom<VideoGame>
}
Generic Implementation - Syntax
struct Name<T: Trait1 + Trait2, U: Trait3> {
field1: T,
field2: U,
}
impl<T: Trait1 + Trait2, U: Trait3> Name<T, U> {
fn func(&self, arg1: T, arg2: U) {}
}
struct Name<T, U>
where
T: Trait1 + Trait2,
U: Trait3,
{
field1: T,
field2: U,
}
impl<T, U> Name<T, U>
where
T: Trait1 + Trait2,
U: Trait3,
{
fn func(&self, arg1: T, arg2: U) {}
}
Demo Generic Structures
struct Dimensions {
width: f64,
height: f64,
depth: f64,
}
struct ConveyBelt<T: Convey> {
pub items: Vec<T>,
}
impl<T: Convey> ConveyBelt<T> {
pub fn add(&mut self, item: T) {
self.items.push(item);
}
}
struct CarPart {
width: f64,
height: f64,
depth: f64,
weight: f64,
part_number: String,
}
impl Default for CarPart {
fn default() -> Self {
Self {
width: 5.0,
height: 1.0,
depth: 2.0,
weight: 3.0,
part_number: "abc".to_owned(),
}
}
}
trait Convey {
fn weight(&self) -> f64;
fn dimensions(&self) -> Dimensions;
}
impl Convey for CarPart {
fn weight(&self) -> f64 {
self.weight
}
fn dimensions(&self) -> Dimensions {
Dimensions {
width: self.width,
height: self.height,
depth: self.depth,
}
}
}
fn main() {
let mut belt: ConveyBelt<CarPart> = ConveyBelt { items: vec![] };
belt.add(CarPart::default());
let mut belt = ConveyBelt { items: vec![] };
}
Activity Generic Structures
trait Body {}
trait Color {}
#[derive(Debug)]
struct Vehicle<B, C>
where
B: Body,
C: Color,
{
body: B,
color: C,
}
impl<B, C> Vehicle<B, C>
where
B: Body,
C: Color,
{
fn new(body: B, color: C) -> Self {
Self { body, color }
}
}
#[derive(Debug)]
struct Car;
impl Body for Car {}
#[derive(Debug)]
struct Truck;
impl Body for Truck {}
#[derive(Debug)]
struct Red;
impl Color for Red {}
#[derive(Debug)]
struct Blue;
impl Color for Blue {}
fn main() {
let red_truck = Vehicle::new(Truck, Red);
let blue_car = Vehicle::new(Car, Blue);
println!("{:?}", red_truck);
println!("{:?}", blue_car);
}
Fundamentals Advanced Memory
Intermediate memory refresh
- All data has a memory address
- Addresses determine the location of data in memory
- Offsets can be used to access adjacent addresses
- Also called indexes/indices
Stack
- Data placed sequentially
- Limited space
- All variables stored on the stack
- Not all data
- Very fast to work with
- Offsets to access
Heap
- Data placed algorithmically
- Slower than stack
- Unlimited space(RAM/disk limits apply)
- Uses pointers
- Pointers are fixed size
- usize data type
- Vectors & HashMaps stored on the heap
- All dynamically sized collections
Example
struct Entry {
id: i32,
}
fn main() {
let data = Entry { id: 5 };
let data_ptr: Box<Entry> = Box::new(data);
let data_stack = *data_ptr;
}
Recap
- Stack
- Sequential memory addresses
- Used for variables
- Limited sizde
- Must know data size ahead of time
- Heap
- Algorithmically calculated memory address
- Used for large amounts of data
- Unlimited size
- Dynamically size data/unknown sized data
Shared Functionality Trait Objects
Trait Object Basics
- Dynamically allocated object
- Runtime generics
- More flexible than generics
- Dynamic Dispatch VS Static Dispatch
- Runtime generics
- Allows mixed types in a collection
- Easier to work with similar data types
- Polymorphic program behavior
- Dynamically change program behavior at runtime
- Easily add new behaviors just by creating a new struct
- Small performance penalty
Creating a Trait Object
trait Clicky {
fn click(&self);
}
struct Keyboard;
impl Clicky for Keyboard {
fn click(&self) {
println!("click clack");
}
}
fn main() {
let keeb = Keyboard;
let keeb_obj: &dyn Clicky = &keeb;
let keeb: &dyn Clicky = &Keyboard;
let keeb: Box<dyn Clicky> = Box::new(Keyboard);
}
Trait Object Parameter - Borrow
fn borrow_clicky(obj:&dyn Clicky) {
obj.click();
}
let keeb = Keyboard;
borrow_clicky(&keeb);
Trait Object Parameter - Move
fn move_clicky(obj: Box<dyn Clicky>) {
obj.click();
}
let keeb = Box::new(Keyboard);
move_clicky(keeb);
Heterogenous Vector
struct Mouse;
impl Clicky for Mouse {
fn click(&self) {
println!("click");
}
}
fn make_clicks(clickers:Vec<Box<dyn Clicky>>) {
for clicker in clickers{
clicker.click();
}
}
let keeb: Box<dyn Clicky> = Box::new(Keyboard);
let mouse: Box<dyn Clicky> = Box::new(Mouse);
let clickers = vec![keeb, mouse];
make_clicks(clickers);
let keeb = Box::new(Keyboard);
let mouse = Box::new(Mouse);
let clickers: Vec<Box<dyn Clicky>> = vec![keeb, mouse];
make_clicks(clickers);
Recap
- Trait objects allow for composite collections
- Slightly less performant than using generics
- Use the dyn keyword when workong with trait objects
- Trait objects can be borrowed using a reference or moved using a box
- Usually want to use a box when storing trait objects in a Vector
Demo Trait Objects
trait Sale {
fn amount(&self) -> f64;
}
struct FullSale(f64);
impl Sale for FullSale {
fn amount(&self) -> f64 {
self.0
}
}
struct OneDollarOffCoupon(f64);
impl Sale for OneDollarOffCoupon {
fn amount(&self) -> f64 {
self.0 - 1.0
}
}
struct TenPercentOffPromo(f64);
impl Sale for TenPercentOffPromo {
fn amount(&self) -> f64 {
self.0 * 0.9
}
}
fn calculate_revenue(sales: &Vec<Box<dyn Sale>>) -> f64 {
sales.iter().map(|sale| sale.amount()).sum()
}
fn main() {
let price = 20.0;
let regular = Box::new(FullSale(price));
let coupon = Box::new(OneDollarOffCoupon(price));
let promo = Box::new(TenPercentOffPromo(price));
// add a type annotations
let sales: Vec<Box<dyn Sale>> = vec![regular, coupon, promo];
println!("total revenue {:?}", calculate_revenue(&sales));
}