Basic questions after first mini-application

Hello

I'm just learning Rust and experimenting with it, so far I actually like it a lot.
I've made a small application which allows you to manage your cats. You can add cats to the database (memory-based) and view them all.

Code:

//Module holding all the structs, enumerations,...
pub mod domain {
    
    use std::string::ToString;

    pub struct Cat {
        name: String,
        age: u8,
        color: CatColor,
        race: CatRace,
    }
    impl Cat {
        pub fn new(name: String, age: u8, color: CatColor, race: CatRace) -> Cat {
            Cat {
                name: name,
                age: age,
                color: color,
                race: race,
            }
        }

        pub fn to_string(&self) -> String {
            let mut s : String = String::new();
            s = format!("Name: {name}\nAge: {age}\nColor: {color}\nRace: {race}",
                    name = self.name,
                    age = self.age,
                    color = self.color.to_string(),
                    race = self.race.to_string(),
                );

            s
        }
    }

    #[derive(Display, Debug)]
    pub enum CatColor {
        Black,
        White,
        Orange,
        Gray,
    }
    impl CatColor {
        pub fn from_u8(value: u8) -> CatColor {
            match value {
                1 => CatColor::Black,
                2 => CatColor::White,
                3 => CatColor::Orange,
                4 => CatColor::Gray,
                _ => {
                    eprintln!("Invalid CatColor selected!");
                    std::process::exit(0);
                }
            }
        }
        
    }

    #[derive(Display, Debug)]
    pub enum CatRace {
        Streetcat,
        AmericanShorthair,
        BritishShorthair,
        Bengal,
        EgyptianMau,
    }
    impl CatRace {
        pub fn from_u8(value: u8) -> CatRace {
            match value {
                1 => CatRace::Streetcat,
                2 => CatRace::AmericanShorthair,
                3 => CatRace::BritishShorthair,
                4 => CatRace::Bengal,
                5 => CatRace::EgyptianMau,
                _ => {
                    eprintln!("Invalid CatRace selected!");
                    std::process::exit(0);
                }
            }
        }
    }
}

//Module holding functions necessary for the userinterface
pub mod user_interface {

    pub mod display {
        use std::io;
        
        //Function to display the start menu
        pub fn display_start_menu() {
            println!("Welcome to the cat database!");
            println!("1) Add cat");
            println!("2) View all cats");
        }

        //Function to display add-cat-menu
        pub fn display_add_cat_menu() {
            println!("Give all the info about the cat!");
        }

        //Function to display view-all-cats-menu
        pub fn display_view_all_cats_menu() {
            println!("List of all the cats:");
        }

        //Function to flush std output
        pub fn flush() {
            io::Write::flush(&mut io::stdout());
        }
    }

    pub mod input {
        use std::io;
        use std::io::stdin;

        //Function to get user input
        pub fn get_u8() -> u8 {
            print!(">");
            //Flush display
            io::Write::flush(&mut io::stdout());

            //Get input and put it in mutable string
            let mut input = String::new();
            stdin()
                .read_line(&mut input)
                .expect("No valid input given...");

            //If parsing succesful return value, else quit program
            match input.trim().parse::<u8>() {
                Ok(i) => {
                    i
                }
                Err(..) => {
                    eprintln!("{} is not a valid number...", input);
                    std::process::exit(0);
                }
            }
        }
        
        //Function to get user input
        pub fn get_str() -> String {
            print!(">");
            //Flush display
            io::Write::flush(&mut io::stdout());

            //Get input and put it in mutable string
            let mut input = String::new();
            stdin()
                .read_line(&mut input)
                .expect("No valid input given...");
    
            input
        }
    }
}

//Module to access, read, and write data
pub mod data_layer {
    use std::vec::Vec;
    use crate::domain::*;

    //To manage data in memory
    pub struct MemoryData {
        cats: Vec<Cat>,
    }
    impl MemoryData {
        pub fn new() -> MemoryData {
            MemoryData { cats: Vec::new() }
        }
        pub fn add(&mut self, cat: Cat) -> bool {
            self.cats.push(cat);
            true
        }
        pub fn read(&mut self) -> &Vec<Cat> {
            &self.cats
        }
    }

    //*TO ADD:* Struct to manage data in text file and database
    //will follow
}

//Module holding logic and binding everything together
pub mod app {
    use crate::user_interface;
    use crate::data_layer;
    use crate::domain::*;

    //Run the application
    pub fn run(db: &mut data_layer::MemoryData) {
        user_interface::display::display_start_menu();
        let option: u8 = user_interface::input::get_u8();
        execute_option(option, db);
        
    }

    //Execute the selected option with corresponding function
    pub fn execute_option(option: u8, db: &mut data_layer::MemoryData) {
        match option {
            1 => {
               add_cat(db); 
            }
            2 => {
                view_all_cats(db);
            }
            _ => {
                eprintln!("Invalid option '{}' selected...", option);
            }
        }
    }

    //Function to add a cat
    pub fn add_cat(db: &mut data_layer::MemoryData) {
        //Displaying menu header
        user_interface::display::display_add_cat_menu();

        //Getting al the info
        println!("Name:");
        let name: String = user_interface::input::get_str();

        println!("Age:");
        let age: u8 = user_interface::input::get_u8();

        println!("Color:\n1) Black 2) White 3) Orange 4) Gray");
        let color: CatColor = CatColor::from_u8(user_interface::input::get_u8());
        
        println!("Race:\n1) Streetcat 2) American Shorthair 3) British Shorthair 4) Bengal 5) Egyptian Mau");
        let race: CatRace = CatRace::from_u8(user_interface::input::get_u8());

        //Create cat object
        let cat: Cat = Cat::new(name, age, color, race);

        //Save cat object
        db.add(cat);

        println!("Cat added...");
    }

    //Function to view all cats
    pub fn view_all_cats(db: &mut data_layer::MemoryData) {
        //Displaying menu header
        user_interface::display::display_view_all_cats_menu();
        //user_interface::display::flush();
        std::io::Write::flush(&mut std::io::stdout());

        //Reading, iterating over, and outputting all the cats
        for cat in db.read() {
            println!("{}", cat.to_string());
        }

    }
}

//Crates
extern crate strum;
#[macro_use]
extern crate strum_macros;

fn main() {
    //Initialize database
    let ref mut db = data_layer::MemoryData::new();

    while true {
        app::run(db);
    }
}

It works fine. But while I was making this 'application' to learn and experiment with Rust I got a few questions about the language, and how to do things efficiently and with using best-practices.

1) Function overloading
If I'm right, Rust doesn't have an option for function overloading. Which gave me a problem for the code on line 112 in the module user_interface::input. I want a function to easily get the user-input. However, that input can be a String, short (i8), integer (i32), float, double,... now I have written two functions: get_u8() and get_str(). What bothers me is that both these functions have almost exactly the same function-body only a different return value. What would be a more efficient way to do this and reducing the use of the same code? Traits? Generic functions?

2) Macros
I don't really get macros. When and why would I use it over a function? println!() for example... Why isn't it just println() ?

3) Difference between & and &mut
What exactly is the difference between these two? From my own understanding & allows you to point to a variable in memory, but you only have read-access. With &mut you can point to a variable in memory but also change the value of that variable you're pointing to. Is that correct?

4) Int to enum
Is there an easy way to cast an integer to an enumeration?
That 0 would equal the first value in the enum and so forth? Like the casting in C++?

Now I used a match-statement in the implementation of my struct. But can this be done more efficiently?

5) Better way to use MemoryData object
On line 158 I have a module called data_layer, this module holds all the code to save, read, and write data. Now I have only a struct called MemoryData. This struct holds the properties and methods to save my cats into memory.

This object is initialized in the main-function and then passed through the rest of the code using reference parameters.
But this is the problem: In the future I also want other structs like FileData and DatabaseData which allows you to persist the data in a file or database and not only the memory.

However, if I want to change the way my program saves it's data, I have to change all the values of the reference-parameters in my code... Is there a more efficient way to do this? Like making the db-variable global or by using generics/interfaces like in C#?

I want to be able to switch between using the MemoryData, FileData and DatabaseData by only changing one line of code. And that'd be line 261. I want to change let ref mut db = data_layer::MemoryData::new();
to let ref mut db = data_layer::FileData::new();
and it should work. So my parameters need a kind of generic type?

6) External crates
If you copy/paste my code and execute it, it probably won't work. Because I used an external dependency called Strum. Is it accepted to use these dependencies as much as you want to solve basic issues? Or is this considered bad because of performance reasons or crates getting outdated,...?

What's best practice to use these dependencies and share code using these dependencies?

7) Return keyword
I can use the return keyword, but I don't need to. Is there a difference when using it or when not using it? What's preferred?

8) Output flush
The only issue I got in my code is that when outputting all the cats, there is a newline after 'Name'. How to get rid of it?

9) Modules
I have used the modules like I kind of would use namespaces in other languages. I grouped the code relevant to each other basing myself on architecture principles like n-tier,... Is the way I've done it good?

I'd actually like to put each module in a separate file. And make a folder for each module having nested modules. So that the nested modules will be come a file each and the parent-module the folder name.

Wouldn't this be better than having everything in one file? How would I do that?

10) Feedback
I'd appreciate feedback on my code and how it's structured. Is everything clear and easy to read? What could be improved? Is it also Rusty enough or is it too OOP?

Thanks a lot!

1 Like

Macros are sort of a feature of last resort, but there's several things they can do that functions can't. println, for example, takes a variable number of arguments, which isn't possible with standard functions; it also implicitly takes its arguments by reference, where in a function you'd have to explicitly do that. So a non-macro println call might have to look like

println("{} + {} = {}", (&a, &b, &c));

which is a lot noisier.

This is kind of correct - the real difference is that &mut is a unique reference, meaning that you're guaranteed to be the only one able to refer to the given memory (which is why mutation is allowed,) where & is a shared reference, meaning that any number of other references could exist simultaneously.

There are some crates that can automate that for you. The match statement is probably the simplest way by default, though.

You're correct about making it generic - what you want is to define a data backing trait that MemoryData, FileData, etc. implement, then make anything that currently takes a MemoryData generic over a type implementing that trait. So for example, the signature of execute_option might become

pub fn execute_option<D: DataBacking>(option: u8, db: &mut D);

Using external crates is perfectly acceptable. Code is most often structured in crates (usually generated by running cargo new,) and as long as you list your dependencies in Cargo.toml users will be able to automatically download and compile all your dependencies.

The only difference is that the return keyword allows early return from functions; otherwise, a function evaluates to the last expression in it. In general, don't use return unless you need to early return (from an if statement, for example.)

You can move modules to separate files by just copying the code inside them to a new file (with the same name) and removing the body of the mod. So if you wanted to move

mod foo {
    struct Foo;
    // ...
}

you'd make a new file called foo.rs containing

struct Foo;
// ...

and replace the original mod definition with

mod foo;
2 Likes

For 1), you can change fn get_u8() -> u8 { into fn get<T: FromStr>() -> T { and .parse::<u8>() into .parse() and now it can return any type that impl FromStr, including the String. This is called return type polymorphism and the return type usually can even be infered.

3 Likes

The normal way to do this would be using generics, provided the implementation is essentially the same.

True function overloading is done using traits, which allows you to create a method with differing implementations, which could then be used with generics to create a function with effectively different implementations for different types.

1 Like

You can also assign custom values to your enums which could make your match expression more clear?

2 Likes

Stability is one of Rust's big promises. Rust's ecosystem is built around semantic versioning. As long as you specify a version number in Cargo.toml (don't use strum = "*"), the code you write today will work tomorrow.

If strum ever publishes a breaking change, it will increment its version number in a semver-breaking manner. Cargo will never update to that version for as long as your Cargo.toml requests the older version. On the other hand, if strum publishes backwards-compatible security updates or bugfixes, it will make a semver-compatible increment to it's version number. In that case, cargo will update to that version of the dependency if you call cargo update or delete Cargo.lock.

If two crates specify different semver incompatible versions of the same dependency, they can still both be used in the same project; cargo will simply build and link two separate versions of that dependency.

Furthermore, for binary crates, Cargo.lock is typically checked into version control. This ensures that anyone who downloads your crate will use the exact same versions of dependencies as you are. This protects you from the case where one of your dependencies accidentally publishes a breaking change in a semver-compatible version bump.

Think of it this way: everything in rust is an expression. There are no statements, only "expression statements" (expressions followed by a semicolon that ignores the expression's value, or blocks of type ()). Any braced block of code is an expression, like the blocks in if { ... } else { ... }, or the block that comprises a function body.

Omitting return is acknowledging the fact that the function body is an expression like any other. As I see it, idiomatic rust never writes an unnecessary return, even in the rare cases where it could increase clarity. (I'm thinking of big nested matches that are longer than a screenful, where you'd have to scroll up or down to see whether the value is stored to a local or if it is returned.)

I believe this is because read_line() includes the newline, so one gets included in name.

Doing name = name.trim().to_string() will fix it.

2 Likes

Also, println! checks the format string at compile time. Macros (especially procedural macros) are often useful when you want to do custom things at build time.

2 Likes

Thanks all! I learned a lot from your replies. I optimized my code using generics and traits.

//Module holding all the structs, enumerations,...
pub mod domain {
    
    use std::string::ToString;

    pub struct Cat {
        name: String,
        age: u8,
        color: CatColor,
        race: CatRace,
    }
    impl Cat {
        pub fn new(name: String, age: u8, color: CatColor, race: CatRace) -> Cat {
            Cat {
                name: name,
                age: age,
                color: color,
                race: race,
            }
        }

        pub fn to_string(&self) -> String {
            let mut s : String = String::new();
            s = format!("Name: {name}\nAge: {age}\nColor: {color}\nRace: {race}",
                    name = self.name,
                    age = self.age,
                    color = self.color.to_string(),
                    race = self.race.to_string(),
                );

            s
        }
    }

    #[derive(Display, Debug)]
    pub enum CatColor {
        Black,
        White,
        Orange,
        Gray,
    }
    impl CatColor {
        pub fn from_u8(value: u8) -> CatColor {
            match value {
                1 => CatColor::Black,
                2 => CatColor::White,
                3 => CatColor::Orange,
                4 => CatColor::Gray,
                _ => {
                    eprintln!("Invalid CatColor selected!");
                    std::process::exit(0);
                }
            }
        }
        
    }

    #[derive(Display, Debug)]
    pub enum CatRace {
        Streetcat,
        AmericanShorthair,
        BritishShorthair,
        Bengal,
        EgyptianMau,
    }
    impl CatRace {
        pub fn from_u8(value: u8) -> CatRace {
            match value {
                1 => CatRace::Streetcat,
                2 => CatRace::AmericanShorthair,
                3 => CatRace::BritishShorthair,
                4 => CatRace::Bengal,
                5 => CatRace::EgyptianMau,
                _ => {
                    eprintln!("Invalid CatRace selected!");
                    std::process::exit(0);
                }
            }
        }
    }
}

//Module holding functions necessary for the userinterface
pub mod user_interface {

    pub mod display {
        use std::io;
        
        //Function to display the start menu
        pub fn display_start_menu() {
            println!("Welcome to the cat database!");
            println!("1) Add cat");
            println!("2) View all cats");
        }

        //Function to display add-cat-menu
        pub fn display_add_cat_menu() {
            println!("Give all the info about the cat!");
        }

        //Function to display view-all-cats-menu
        pub fn display_view_all_cats_menu() {
            println!("List of all the cats:");
        }

        //Function to flush std output
        pub fn flush() {
            io::Write::flush(&mut io::stdout());
        }
    }

    pub mod input {
        use std::io;
        use std::io::stdin;
        use std::str::FromStr;

        //Function to get user input
        pub fn get<T: FromStr>() -> T {
            print!(">");
            //Flush display
            io::Write::flush(&mut io::stdout());

            //Get input and put it in mutable string
            let mut input = String::new();
            stdin()
                .read_line(&mut input)
                .expect("No valid input given...");
    
            match input.trim().parse() {
                Ok(i) => i,
                Err(..) => {
                    eprintln!("Corrupted input '{}' given!", input);
                    std::process::exit(0);
                }
            }
        }
    }
}

//Module to access, read, and write data
pub mod data_layer {
    use std::vec::Vec;
    use crate::domain::*;

    //Trait to generalize the different type of data management options
    pub trait DataManager {
        //Initialize data manager
        fn new() -> Self;

        //Add to the database
        fn add(&mut self, cat: Cat) -> bool;

        //Read from the database
        fn read(&mut self) -> &Vec<Cat>;
    }

    //To manage data in memory
    pub struct MemoryData {
        cats: Vec<Cat>,
    }
    impl DataManager for MemoryData {
        fn new() -> MemoryData {
            MemoryData { cats: Vec::new() }
        }
        fn add(&mut self, cat: Cat) -> bool {
            self.cats.push(cat);
            true
        }
        fn read(&mut self) -> &Vec<Cat> {
            &self.cats
        }
    }

    //*TO ADD:* Struct to manage data in text file and database
    //will follow
}

//Module holding logic and binding everything together
pub mod app {
    use crate::user_interface;
    use crate::data_layer;
    use crate::domain::*;

    //Run the application
    pub fn run(db: &mut data_layer::MemoryData) {
        user_interface::display::display_start_menu();
        let option: u8 = user_interface::input::get();
        execute_option(option, db);
        
    }

    //Execute the selected option with corresponding function
    pub fn execute_option<D: data_layer::DataManager>(option: u8, db: &mut D) {
        match option {
            1 => {
               add_cat(db); 
            }
            2 => {
                view_all_cats(db);
            }
            _ => {
                eprintln!("Invalid option '{}' selected...", option);
            }
        }
    }

    //Function to add a cat
    pub fn add_cat<D: data_layer::DataManager>(db: &mut D) {
        //Displaying menu header
        user_interface::display::display_add_cat_menu();

        //Getting al the info
        println!("Name:");
        let name: String = user_interface::input::get();

        println!("Age:");
        let age: u8 = user_interface::input::get();

        println!("Color:\n1) Black 2) White 3) Orange 4) Gray");
        let color: CatColor = CatColor::from_u8(user_interface::input::get());
        
        println!("Race:\n1) Streetcat 2) American Shorthair 3) British Shorthair 4) Bengal 5) Egyptian Mau");
        let race: CatRace = CatRace::from_u8(user_interface::input::get());

        //Create cat object
        let cat: Cat = Cat::new(name, age, color, race);

        //Save cat object
        db.add(cat);

        println!("Cat added...");
    }

    //Function to view all cats
    pub fn view_all_cats<D: data_layer::DataManager>(db: &mut D) {
        //Displaying menu header
        user_interface::display::display_view_all_cats_menu();
        //user_interface::display::flush();
        std::io::Write::flush(&mut std::io::stdout());

        //Reading, iterating over, and outputting all the cats
        for cat in db.read() {
            println!("{}", cat.to_string());
        }

    }
}

//Crates
extern crate strum;
#[macro_use]
extern crate strum_macros;

fn main() {
    use crate::data_layer::DataManager;

    //Initialize database
    let ref mut db = data_layer::MemoryData::new();

    while true {
        app::run(db);
    }
}
1 Like