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 :).