Rust Ramblings: Learning Rust 3
Contents: Structs
Apparently Rust is the future so I thought I would hop on board. This is more of a reference for myself, than a tutorial. This is part 3, all about structs.
A struct describes and encapsulates properties of an object. There is 3 variants of structs: C-like structs, Tuple structs, Unit structs. Examples:
// C-like structs - like a class in an object-oriented class
// Example:
struct Person {
name: String,
age: u8,
has_hat: boolean
}
// Tuple structs - a parathesized list-like tuples
// Example:
struct Point(u8, u8);
// Union struct - a struct with no members
// Example:
struct Hat;
A Triangle
Here is a triangle struct, it is illustrative, but not efficient.
// Here is the triangle struct, it has 3 side lengths: a, b, and c
struct Triangle {
a: u32,
b: u32,
c: u32
}
fn main() {
// Initialize the triangle:
let t = Triangle {
a: 3, b: 2, c: 1
};
// We can call methods with the triangle:
println!("The triangle is {}", triangle_type(&t));
println!("The triangle has a perimeter {}", triangle_perimeter(&t));
}
// Triangle type
fn triangle_type(triangle: &Triangle) -> &str {
if triangle.a == triangle.b && triangle.b == triangle.c {
"equilateral"
} else if triangle.a == triangle.b || triangle.b == triangle.c || triangle.a == triangle.c {
"isosceles"
} else {
"scalene"
}
}
// Get triangle perimeter
fn triangle_perimeter(triangle: &Triangle) -> u32 {
triangle.a + triangle.b + triangle.c
}
Improving the Triangle - Impl
Implementations (Impl) are used to define methods for Rust structs and enums. Implementations are defined by the keyword impl. They contain functions that belong to an instance of a particular type. Here is an example of the same triangle struct with impl
// Here is the triangle struct, it has 3 side lengths: a, b, and c
struct Triangle {
a: u32,
b: u32,
c: u32
}
// Here is the impl, with all the methods belonging to the struct
impl Triangle {
// to access member attributes we use self.[attribute name]
fn perimeter(&self) -> u32 {
self.a + self.b + self.c
}
// we can also use the methods from other instances of the struct
fn is_bigger(&self, other: &Triangle) -> bool {
self.perimeter() > other.perimeter()
}
// We also can change member attributes
fn reset_side_a(&mut self, a_new: u32) {
self.a = a_new;
}
}
fn main() {
let mut t = Triangle {
a: 3,
b: 2,
c: 1
};
let t2 = Triangle {
a: 2,
b: 1,
c: 1
};
println!("The triangle has a perimeter of {}", t.perimeter());
println!("The triangle A is bigger than B: {}", t.is_bigger(&t2));
t.reset_side_a(4);
println!("The triangle has side a of length {}", t.a);
}
Traits
There is two types of implementations:
- inherent implementations
- trait implementations
Traits are similiar to interfaces in Object-Oriented Programming languages, and they are used to define the functionality a type must provide. Here is an example, with traits and inheritence, it not the best example but I couldn’t think of a better one. The important thing here I think is the seperation between the object’s functionality (with traits), and the objects attributes. This type of design seems common in the design of rust applications.
// Take out what makes up a Weapon put that functionality into traits (for inheritence)
// Weapons - I couldn't think of a better example
// Sword, Blaster, Lightsaber, Bow
// When we use generics like below, we can not have other methods that don't use generics in the trait (hence the second trait Damages)
trait Damage {
fn damage_action(&self);
}
trait Damages {
fn do_damage(&self, u32) -> u32;
}
// A weapon has two traits the Weapon type, and action, they are defined for each type of weapon (child object)
trait Weapon {
fn weapon_type(&self) -> &str;
fn action(&self) -> &str;
}
// Generics - only one definition of damage_action() for all weapon types
impl<T> Damage for T where T: Weapon {
fn damage_action(&self) {
println!("The enemy {} with a {}", self.action(), self.weapon_type());
}
}
// The child structs
struct Sword {}
struct Blaster {}
struct Lightsaber {}
// We define the traits of the Weapon: Sword
impl Weapon for Sword {
fn weapon_type(&self) -> &str {
"Sword"
}
fn action(&self) -> &str {
"slice"
}
}
impl Weapon for Blaster {
fn weapon_type(&self) -> &str {
"Blaster"
}
fn action(&self) -> &str {
"shoot"
}
}
impl Weapon for Lightsaber {
fn weapon_type(&self) -> &str {
"Lightsaber"
}
fn action(&self) -> &str {
"slash"
}
}
// We define how much damage the sword does:
impl Damages for Sword {
fn do_damage(&self, health: u32) -> u32 {
if health > 8 {
health - 8
} else {
0
}
}
}
impl Damages for Blaster {
fn do_damage(&self, health: u32) -> u32 {
if health > 2 {
health - 2
} else {
0
}
}
}
// the health param in this method is prefixed by an underscore because it is never used
impl Damages for Lightsaber {
fn do_damage(&self, _health: u32) -> u32 {
println!("You are dead!");
0
}
}
fn main() {
let mut health = 100;
// Create an instance of each weapon
let sword = Sword {};
let blaster = Blaster {};
let lightsaber = Lightsaber {};
// We can the methods for each of the weapon types the same way
sword.damage_action();
health = sword.do_damage(health);
println!("Health is {}", health);
blaster.damage_action();
health = blaster.do_damage(health);
println!("Health is {}", health);
lightsaber.damage_action();
health = lightsaber.do_damage(health);
println!("Health is {}", health);
This is the output:
crazyeights@es-base:~/Desktop/RustyBucket$ ./ex_traits
The enemy slice with a Sword
Health is 92
The enemy shoot with a Blaster
Health is 90
The enemy slash with a Lightsaber
You are dead!
Health is 0
Aside: Searching a list of objects:
We can also search lists of objects for the indices of elements with a particular property:
struct Person {
id: u8,
name: String,
has_hat: bool
}
fn person_factory(id: u8, name: String, has_hat: bool) -> Person {
Person {
id: id,
name: name,
has_hat: has_hat
}
}
fn main() {
let mut p = Vec::new();
p.push(person_factory(12, "Joe".to_string(), true));
p.push(person_factory(8, "Bob".to_string(), false));
// Here we use |i| instead of |&i|, we iterate over the values instead of references:
let index = p.iter().position(|i| i.id == 8).unwrap();
println!("{}", index);
println!("Name of Person with id 8: {}", p[index].name);
}
A Grocery Store Application
I created a basic CLI grocery store application where we have a customer with a fixed balance, and a finite amount of merchandise. This is very basic second year uni level stuff, but a step up from Hello World. Its not explained very well, but the appication structure is very similiar to most other languages.
use std::fmt::{self, Formatter, Display};
use std::io;
// A Grocery Item object - holds a item you would buy at the grocery store
struct GroceryItem {
name: String,
category: String,
price: f32,
quantity: u32
}
// Display - override the way the object is displayed/printed
impl Display for GroceryItem {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "Name: {} \nCategory: {}, Price: {}, Quatity: {}\n--------------------", self.name, self.category, self.price, self.quantity)
}
}
// a method to create GroceryItem objects from input params
fn item_factory(name: String, category: String, price: f32, quantity: u32) -> GroceryItem {
GroceryItem{
name: name,
category: category,
price: price,
quantity: quantity
}
}
// A customer struct
// the cart Vec is an array of indices of grocery items
struct Customer {
account_no: i32,
name: String,
balance: f32,
cart: Vec<usize>
}
// a method to create Customer objects from input params
fn customer_factory(account_no: i32, name: String, balance: f32) -> Customer {
Customer{
account_no: account_no,
name: name,
balance: balance,
cart: Vec::new()
}
}
// Add an item to the cart Vec by index in the GroceryStore object items list
impl Customer {
fn add_item_to_cart(&mut self, item_id: usize) {
self.cart.push(item_id);
}
}
impl Display for Customer {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "Account No.: {}\nName: {}\nBalance: {}\n--------------------", self.account_no, self.name, self.balance)
}
}
//Grocery Store object - has a name, a location, a list of items, and a list of customers
struct GroceryStore {
name: String,
location: String,
items: Vec<GroceryItem>,
customers: Vec<Customer>
}
impl GroceryStore {
fn add_grocery_item(&mut self, gi: GroceryItem) {
self.items.push(gi);
}
fn add_customer(&mut self, c: Customer) {
self.customers.push(c);
}
fn print_customer(&self, cid: usize) {
//Prints the customer info as defined in the Display implementation earlier
println!("{}", self.customers[cid]);
// Prints the contents of the cart and the total
let mut total: f32 = 0.0;
println!("CART: ");
for i in &self.customers[cid].cart {
println!("1 x {}, {} ($ {})", self.items[*i].name, self.items[*i].category, self.items[*i].price);
total += self.items[*i].price;
}
println!("------------------------------------");
println!("Total: {}", total);
}
// Gets the total of the items in the cart
fn get_cart_total_for_customer(&self, cid: usize) -> f32 {
let mut total: f32 = 0.0;
for i in &self.customers[cid].cart {
total += self.items[*i].price;
}
total
}
// Customer checks out, if they do not have enough in their account nothing is purchased
fn customer_checkout(&mut self, cid: usize) {
let total = self.get_cart_total_for_customer(cid);
if total > self.customers[cid].balance {
println!("Insufficient Funds!");
}else{
self.customers[cid].cart.clear();
self.customers[cid].balance -= total;
println!("Thank you for your purchase!");
}
}
fn print_store(&self) {
println!("{}", self.name);
println!("====================");
println!("Location: {}", self.location);
println!("Items: ");
for i in &self.items {
println!("{}", i);
}
}
fn print_customers(&self) {
for c in &self.customers {
println!("{}", c);
}
}
// List grocery items for user to select
fn list_items(&self) {
let mut i = 0;
for item in &self.items {
println!("{} - {} ($ {}, {} in stock)", i, item.name, item.price, item.quantity);
i += 1;
}
}
fn print_customers_for_selection(&self) {
for cust in &self.customers {
println!("{} - {}", cust.account_no, cust.name);
}
}
// return the index of the selected customer by account no.
fn get_current_customer(&self, cid: i32) -> usize {
let index = self.customers.iter().position(|i| i.account_no == cid).unwrap();
index
}
// Empty the customers cart, returning the item to the store stock.
fn empty_cart(&mut self, cid: usize) {
while let Some(item) = self.customers[cid].cart.pop() {
self.items[item].quantity += 1;
}
}
// Add an item to the cart by index:
fn add_to_cart(&mut self, cid: usize, item: usize) {
if item > self.items.len() - 1 {
println!("Invalid Option");
} else if self.items[item].quantity > 0 {
self.customers[cid].add_item_to_cart(item);
self.items[item].quantity -= 1;
} else {
println!("Item: {} not in stock", self.items[item].name);
}
}
}
fn main() {
// Create the grocery store
let mut gs = GroceryStore {
name: "Frank's Grocery".to_string(),
location: "123 Rust Lane".to_string(),
items: Vec::new(),
customers: Vec::new()
};
// Create some grocery items
gs.add_grocery_item(item_factory("Carrots".to_string(), "Produce".to_string(), 4.99, 12));
gs.add_grocery_item(item_factory("Cookies".to_string(), "Bakery".to_string(), 6.50, 8));
// Create some customers
gs.add_customer(customer_factory(12, "John Smith".to_string(), 50.0));
gs.add_customer(customer_factory(10, "Jane Doe".to_string(), 12.0));
// Print the store details
gs.print_store();
println!("------------------");
gs.print_customers();
// Allow the user to select a customer to "shop" as by account no.
println!("\nCUSTOMERS:");
gs.print_customers_for_selection();
println!("\nEnter a customer account no.:");
let option = get_option();
let c = gs.get_current_customer(option);
println!("Welcome {}\n", gs.customers[c].name);
// action loop:
print_menu();
loop {
println!("Enter an option (0-5):");
let option = get_option();
println!("");
if option == 0 {
println!("Thanks for coming to {}", gs.name);
break;
} else if option == 1 {
gs.list_items();
} else if option == 2 {
println!("Enter an item:");
let i = get_option();
gs.add_to_cart(c, i as usize);
} else if option == 3 {
gs.print_customer(c);
} else if option == 4 {
gs.customer_checkout(c);
} else if option == 5 {
gs.empty_cart(c);
} else {
println!("Invalid Option");
}
}
}
fn print_menu() {
println!("Menu:\n1 - List Items\n2 - Add to Cart\n3 - View Customer\n4 - Checkout\n5 - Empty Cart\n0 - Exit\n\n");
}
// Get an integer from stdin
fn get_option() -> i32 {
let mut input = String::new();
io::stdin().read_line(&mut input).expect("can't process input");
let n = input.trim().parse().unwrap();
n
}
Here is a sample of its output, this just shows the emptying of the cart, and nothing else:
crazyeights@es-base:~/Desktop/RustyBucket$ ./groceries
Frank's Grocery
====================
Location: 123 Rust Lane
Items:
Name: Carrots
Category: Produce, Price: 4.99, Quatity: 12
--------------------
Name: Cookies
Category: Bakery, Price: 6.5, Quatity: 8
--------------------
------------------
Account No.: 12
Name: John Smith
Balance: 50
--------------------
Account No.: 10
Name: Jane Doe
Balance: 12
--------------------
CUSTOMERS:
12 - John Smith
10 - Jane Doe
Enter a customer account no.:
12
Welcome John Smith
Menu:
1 - List Items
2 - Add to Cart
3 - View Customer
4 - Checkout
5 - Empty Cart
0 - Exit
Enter an option (0-5):
2
Enter an item:
1
Enter an option (0-5):
2
Enter an item:
1
Enter an option (0-5):
1
0 - Carrots ($ 4.99, 12 in stock)
1 - Cookies ($ 6.5, 6 in stock)
Enter an option (0-5):
5
Enter an option (0-5):
1
0 - Carrots ($ 4.99, 12 in stock)
1 - Cookies ($ 6.5, 8 in stock)
Enter an option (0-5):
0
Thanks for coming to Frank's Grocery
Next I hope to learn more specific systems things, as I kinda know some Rust now :).