4
\$\begingroup\$

I am learning Rust and would like a review to make sure I am following best Rust practices before continuing. The user enters a number between 1 and 100 up to 6 times to guess the randomly-generated number the system knows.

use std::io; use rand::Rng; fn main() { let num = get_num(); let mut guessed = false; for _ in 0..6 { let guess = get_guess(); match guess { x if x < num => println!("Your number is too low"), x if x > num => println!("Your number is too high"), _ => { println!("Correct!"); guessed = true; break; } } } if !guessed { println!("Ran out of guesses. The number was {}", num); } } fn get_num() -> i32 { let mut rng = rand::rng(); let num: i32 = rng.random_range(1..=100); num } fn get_guess() -> i32 { let stdin = io::stdin(); let mut guess = None; println!("Guess a number between 1 and 100"); while let None = guess { let mut buffer = String::new(); let _ = stdin.read_line(&mut buffer); guess = match buffer.trim().parse::<i32>() { Ok(value) => Some(value), Err(_) => { println!("Invalid guess; try again"); None } }; } guess.expect("Can't be reached") } 
\$\endgroup\$

    3 Answers 3

    2
    \$\begingroup\$

    I don't know Rust, but I do like guessing games.

    Here are some suggestions to enhance the user experience.

    Progress

    It would be convenient for the user to see an ordered list of guesses so far. For example, assuming my first 3 guesses are 50, 75, 62, it would be nice to see a message like:

    You have guessed 50, 62, 75 

    Valid input

    The prompt shows a range of between 1 and 100. If I enter 0, the code accepts that as a valid guess. It would be nice to print a message that 0 is not a valid guess, and it would be extra nice if that guess were not included in one of the 6 guesses. In other words, it would be nice to support retries. The code already checks for non-numeric input, and it does retries for that.

    The same applies to other values outside the range, like 120 and -55.

    Repeat guesses

    It would be nice to also support retries if the user enters the same guess twice. If you implement the progress suggestion above, you will already have a list of previous guesses. You could print a message that the guess was already tried and don't count the repeat guess as 1 of the 6.

    \$\endgroup\$
    2
    • \$\begingroup\$Thanks. For some reason SE isn't recognizing the account I posted this under, so unless I can figure that out, I won't be able to accept any answers. It's weird, but it says no account exists for my email that I used to sign in multiple times yesterday...\$\endgroup\$
      – hosch250
      CommentedFeb 28 at 17:37
    • \$\begingroup\$@hosch250: You're welcome. That is odd about your account. When I look at your profile, I see you have a different name on the only other SE account of yours that I can see: Return of the Hosch. Maybe you can ping a Mod in 2nd monitor chat\$\endgroup\$
      – toolic
      CommentedFeb 28 at 18:07
    2
    \$\begingroup\$

    You are using guessed in main() and guess in get_guess() to signal other logic, but that is not needed if you simply incorporate early returns:

    fn main() { let num = get_num(); for _ in 0..6 { let guess = get_guess(); match guess { x if x < num => println!("Your number is too low"), x if x > num => println!("Your number is too high"), _ => { println!("Correct!"); return; } } } println!("Ran out of guesses. The number was {}", num); } ... fn get_guess() -> i32 { let stdin = io::stdin(); println!("Guess a number between 1 and 100"); loop { let mut buffer = String::new(); let _ = stdin.read_line(&mut buffer); match buffer.trim().parse::<i32>() { Ok(value) => return value, Err(_) => { println!("Invalid guess; try again"); } } } } 

    The smell was the .expect() which is generally to be avoided if at all possible.


    You are ignoring errors from read_line which means any errors reading from stdin will likely result in an infinite loop of failures (e.g. if stdin was closed). In this case a .unwrap() would often be acceptable since your program cannot meaningfully continue without input.


    If you want to be slick, you can use .cmp() to get whether a value is greater than, less than, or equal in a type-safe way:

    use std::cmp::Ordering; match guess.cmp(&num) { Ordering::Less => println!("Your number is too low"), Ordering::Greater => println!("Your number is too low"), Ordering::Equal => { println!("Correct!"); return; } } 
    \$\endgroup\$
      1
      \$\begingroup\$

      Building on top of existing answers, here are a few more observations:

      Strange loop

      This is obviously correct:

      while let None = guess 

      ... but rather weird way to check if an Option is None. Consider just

      while guess.is_none() 

      Strange guarded match

      Yes, you can match guess { x if x < num => ... }, but that's usually helpful if x is some part of a binding pattern, not the whole pattern itself. I'd agree if it were Ok(Some(x)) if x < num, but for a simple pattern an if/else chain is somewhat more obvious:

       if guess < num { println!("Your number is too low"); } else if guess > num { println!("Your number is too high"); } else { println!("Correct!"); guessed = true; break; } 

      Control flow

      You had to introduce a guessed variable just to check whether the loop has exited "normally". Nothing prevents you from using return in main function:

      fn main() { let num = get_num(); for _ in 0..6 { let guess = get_guess(); if guess < num { println!("Your number is too low"); } else if guess > num { println!("Your number is too high"); } else { println!("Correct!"); return; } } println!("Ran out of guesses. The number was {}", num); } 

      Variable naming

      You chose good function names, they convey the meaning perfectly and match the runtime behaviour. My only suggestion would be to replace num with target - i32 type already conveys it's a number, the variable name should explain the purpose of it. The same applies to get_num function.

      Personal preference: match vs transformations

      I would rather write

       guess: Option<i32> = None; // ... guess = buffer .trim() .parse() .inspect_err(|_| println!("Invalid guess; try again")) .ok(); 

      Note that a) I moved the i32 annotation to the definition site so that .parse can be used unqualified, and b) I rewrote match with native Result methods. To me this reads a bit easier: .ok() transforms a Result to an Option, and inspect_error contains something we want to do if the result was Err. But the early return approach in @kmdreko answer is even nicer as it avoids a complicated Result-Option-inner value conversion.

      Similarly, your last .expect("Can't be reached") should be .unwrap(): it doesn't accept a message string and better conveys the meaning of "I'm absolutely confident this should always pass" while .expect("Message") is closer to "This shouldn't normally happen, but here's a helpful message if it somehow does".

      \$\endgroup\$

        Start asking to get answers

        Find the answer to your question by asking.

        Ask question

        Explore related questions

        See similar questions with these tags.