First Exercises in Rust; feedback appreciated!

I set a goal for myself to learn Rust this year. I've got a fair bit of experience in python, but I'm not a coder by trade, so it's mostly self-taught. I use it for data collection and analysis primarily.

I'm reading Steve Klabnik's book "The Rust Programming Language (2e)", and just finished the 3rd chapter which has three exercises: Convert temperatures between C and F, generate the nth Fibonacci number, and print the lyrics to the worst Christmas Carol ever conceived.

I did it all in one project, coding each exercise as its own function. I intentionally only used information presented in the book to this point, so I'm certain there are better ways to do a lot of this. I'd appreciate feedback on style, things to look out for, and maybe previews of those "better ways" to have in mind as I continue in the book.

My main() function just gives a simple menu to select which exercise to run -- it was a convenient way for me to work through each of the three problems without having to either make different projects for each or run through each completed problem before testing the one I'm working on.

use std::io;

fn main() {
    // Set up a simple menu to choose which exercise code to run. This helps to not have to run
    // every single exercise every time I do cargo run.
    loop {
        println!("Chapter 3 Exercises");
        println!("-------------------");
        println!("1. Exercise 1");
        println!("2. Exercise 2");
        println!("3. Exercise 3");
        println!("9. Quit");

        let mut input = String::new();
        io::stdin()
            .read_line(&mut input)
            .expect("failed to read line");
        let mode: usize = match input.trim().parse() {
            Ok(num) => num,
            Err(_) => continue, // if anything weird happens, just ignore it and start over.
        };

        if mode == 1 {
            // Run the first exercise
            exercise1();

        } else if mode==2 {
            // Run the second exercise
            exercise2();

        } else if mode==3 {
            // Run the third exercise
            exercise3();

        } else if mode==9 {
            // Quit the program
            break;

        } else {
            // Throw a fit if anything else happens, and start over.
            println!("Invalid option {mode}");
            continue;
        }
    }
}

Exercise 1

fn exercise1() {
    println!("Exercise 1");
    println!("----------");

    // select the conversion mode
    let mut input = String::new();
    println!("1. C->F");
    println!("2. F->C");

    io::stdin()
        .read_line(&mut input)
        .expect("failed to read line");

    let mode: usize = match input.trim().parse() {
        Ok(num) => num,
        Err(_) => 999999, // if non-numerical data is entered, set to a value that
                          // triggers a return to the main menu.
    };


    if mode == 1 {
        // Convert Celsius to Fahrenheit

        // Get Celsius Value
        let mut input = String::new();
        println!("Enter the number of degrees in Celsisus.");
        io::stdin()
            .read_line(&mut input)
            .expect("failed to read line");
        let c: f32 = match input.trim().parse() {
            Ok(num) => num,
            Err(_) => return, // If non-numerical data is given, just return out
                              // of the function and go back to the menu.
        };

        // Convert to Fahrenheit
        let f: f32 = c*9.0/5.0+32.0;
        println!("{c} deg C is {f} deg F.");

    } else if mode == 2 {
        // Convert Fahrenheit to Celsius

        // Get Fahrenheit Value
        let mut input = String::new();
        println!("Enter the number of degrees in Fahrenheit.");
        io::stdin()
            .read_line(&mut input)
            .expect("failed to read line");
        let f: f32 = match input.trim().parse() {
            Ok(num) => num,
            Err(_) => return, // If non-numerical data is given, just return out
                              // of the function and go back to the menu.
        };

        // Convert to Celsius
        let c: f32 = (f-32.0)*5.0/9.0;
        println!("{f} deg F is {c} deg C.");

    } else {
        // Any numerical value other than 1 or 2 is ignored, and the user
        // is sent back to the main menu.
        println!("Invalid input; returning to main menu.");
        return;
    }

    println!("Exercise 1 concluded.")
}

Exercise 2

fn exercise2() {
    println!("Exercise 2");
    println!("----------");

    // Have the user provide which Fibonnaci number to calculate. Note that for i64, the
    // 93rd Fibonacci number is the largest that will not trigger an overflow.
    let mut input = String::new();
    println!("Enter the nth Fibonnaci number to calcluate (max 93).");
    io::stdin()
        .read_line(&mut input)
        .expect("failed to read line");
    let n: isize = match input.trim().parse() {
        Ok(num) => num,
        Err(_) => -1,
    };

    // We need to track the previous two values to calculate the next.
    let mut value: u64 = 1; // holds the most recent value
    let mut last_value: u64 = 0; // holds the previous value
    let mut old_value: u64; // temporary placeholder to handle changing to the next number

    // Step through the Fibonnaci sequence n times.
    for _i in 1..n {
        old_value = value; // temporarily hold a copy of the current value
        value += last_value; // new value is the current plus the previous
        last_value = old_value; // set the previous to what the current value was before
    }

    println!("The {n}th Fibonnaci number is {value}");
    println!("Exercise 2 concluded.");
}

Exercise 3

fn exercise3() {
    println!("Exercise 3");
    println!("----------");

    // Set up arrays for the ordinal day and the gifts given
    let days: [&str; 12] = ["first", "second", "third", "fourth", "fifth", "sixth",
                           "seventh", "eigth", "ninth", "tenth", "eleventh", "twelfth"];

    let gifts: [&str; 12] = ["a partridge in a pear tree!",
                            "two turtle doves,",
                            "three french hens,",
                            "four calling birds,",
                            "five golden rings,",
                            "six geese a-laying,",
                            "seven swans a-swimming,",
                            "eight maids a-milking,",
                            "nine ladies dancing,",
                            "ten lords a-leaping,",
                            "eleven pipers piping,",
                            "twelve drummers drumming,"];

    // Step through the 12 verses
    for i in 0..12 {
        // Each verse starts with this line:
        println!("On the {} day of Christmas, my true love gave to me:", days[i]);
        // If it's the first verse, we have to do something special, since there is no 'and' in
        // this verse.
        if i==0 {
            println!("    {}", gifts[0]);

        } else {
            // for other verses, we need to step through each of the gifts in reverse order.
            for j in (0..i+1).rev() {
                // at j==0, we're back to the first gift, which gets the 'and' added
                if j==0 {
                    println!("    and {}", gifts[j]);
                } else {
                println!("    {}", gifts[j]);
                }
            }
        }
    }

    println!("Exercise 3 concluded.");
}

Any thoughts, improvements, things to watch out for are welcome!

fn main

        // if anything weird happens, just ignore it and start over.
        let Ok(mode) = input.trim().parse::<usize>() else { continue };

This (let else) is probably a pattern that hasn't been introduced yet. You can use it when you just want to match one thing, and the else blocks diverges (returns, continues, breaks...).

The turbofish (::<usize>) is a way to annotate the parse call, which I reached for because I like it better than the alternative here:

        let Ok(mode): Result<usize, _> = input.trim().parse() else { continue };

...or you could actually drop the annotation altogether and it will infer i32 in this case. Often that matters, but not in this case since you don't distinguish parsing errors from invalid values and your set of valid values is small.

        match mode {
            1 => exercise1(),
            2 => exercise2(),
            3 => exercise3(),
            // Quit
            9 => break,
            // Throw a fit if anything else happens, and start over.
            _ => println!("Invalid option {mode}"),
        }

This is possible and ergonomic since you're just checking literals. The continue isn't needed after throwing a fit, since you're at the bottom of the loop anyway.

fn exercise1

    let mode: usize = input.trim().parse().unwrap_or(999999);

...but you know what, nah, scratch that. Let's just not use so many magic values.

    let c_to_f = match input.trim().parse() {
        Ok(1) => true,
        Ok(2) => false,
        _ => {
            // Any numerical value other than 1 or 2 is ignored, and the user
            // is sent back to the main menu.
            println!("Invalid input; returning to main menu.");
            return;
        }
    };

    if c_to_f {

(This is inching towards stricter typing -- "make invalid states unrepresentable". But we're sticking with bool instead of a custom enum or something.)

Not a biggie, but you can reuse your input string buffer. Just .clear() it first.

    if c_to_f {
        // Get Celsius Value
        input.clear();
        println!("Enter the number of degrees in Celsisus.");
        io::stdin()
            .read_line(&mut input)
            .expect("failed to read line");

fn exercise2

You should check that they didn't ask for something too big here, or use checked arithmetic. Otherwise you'll overflow and either panic or give bogus results.

    let n = match input.trim().parse::<u8>() {
        Ok(num) if num <= 93 => num,
        // Ignore the rest
        _ => return,
    };

Creating an iterator could be fun here, but you're perhaps not that far into the material yet.

fn exercise3

Mainly I'd refactor the special cases to be on the outsides and not the insides.

    // The first verse is special, since there is no 'and'
    println!("On the {} day of Christmas, my true love gave to me:", days[0]);
    println!("    {}", gifts[0]);
    
    // Step through the remaining verses
    for i in 1..12 {
        // Each verse starts with this line:
        println!("On the {} day of Christmas, my true love gave to me:", days[i]);

        // Step through all but the first gift in reverse order, starting today
        for gift in gifts[1..i+1].iter().rev() {
            println!("    {gift}");
        }

        // and for the first gift, prepend `and`
        println!("    and {}", gifts[0]);
    }

I also replaced some of your index-based looping with an interator.


The main other thing that stood out to me was the lack of error propagation (lots of expect and silent returns), but that's probably another area of the book you haven't reached yet.

You could factor out some of your line reading logic to a separate function. You could even make it generic and have a "read one line with this type" function.

5 Likes

Thank you! That's actually really interesting input; I was able to follow what you were doing and while the syntax wasn't familiar, I could see what it was doing and why you would suggest that. I'll look forward to seeing that later in the book and come back to these.

I really appreciate the stepped approach in some of your response; it's helpful to see "this is a better way that's close to what you did; and here's a more advanced way to see what you're looking for in the future" and it answers my question very well.