Guessing Game stdin Result rand — the guess that is not a number
stdin writes a String. parse makes a u32. cmp needs the number. The rest of the game assumes the number.
<!-- hal:authoritative:yaml -->
stdin writes a String. parse makes a u32. cmp needs the number. The rest of the game assumes the number.
§I — Frame
Duha session 02. Second fire of the TRPL spine. The chapter is Programming a Guessing Game. One program, four syllabus moves: an argument of a sort, Result, rand from crates.io, a test of the compare.
Session 01 left you on a crate. cargo new wrote Cargo.toml and src/main.rs. Today you start a second crate named guessing_game. Do not reopen hello_cargo. Do not compile a lone main.rs with rustc. The book never returns to the file that is not a crate.
The program does this. It picks an integer from 1 through 100. It asks for a guess. It says too low, too high, or you win. Then it exits.
Coin the name now: the guess that is not a number.
read_line appends whatever the player typed, plus the newline, into a String. That value is text. It is not a u32. cmp against the secret will refuse it until you trim and parse. Shadow the name. Keep the text in mind when you see the type error.
Done-criteria from the syllabus: you can walk the game. Args of a sort. Result. crates.io rand. A test of the compare.
§II — cargo new, then an argument of a sort
From the projects directory you made in Ch.1:
$ cargo new guessing_game
$ cd guessing_game
$ cargo run
Cargo.toml names the package guessing_game, edition 2024, empty [dependencies]. src/main.rs is still Hello, world!. cargo run compiles and prints it. From here the book iterates in this file.
Replace the body with Listing 2-1.
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
The argument of a sort is stdin, not argv. std::env::args is a later chapter. Today the player types into the terminal after the prompt. use std::io; brings the input library in. The prelude does not include it. Without the use, the same call is std::io::stdin.
let mut guess = String::new(); binds a growable UTF-8 buffer. Variables are immutable by default. read_line must append, so the binding is mut. String::new is an associated function. The :: is the tell. It returns an empty String.
io::stdin() returns a handle to the terminal. .read_line(&mut guess) appends into that buffer. The & is a reference. References are immutable by default, so the call is &mut guess, not &guess. Chapter 4 is the references chapter. Today you need the mut borrow so the method can write.
read_line does not overwrite. It appends. A fresh String::new() each time through the later loop keeps that fact from biting you.
{guess} in println! is a placeholder. The book also shows empty {} filled from later arguments: println!("x = {x} and y + 2 = {}", y + 2);.
Run it.
$ cargo run
Guess the number!
Please input your guess.
6
You guessed: 6
You have input. You do not yet have a number. The printed 6 still carries the newline; the prompt just hides it. That is the guess that is not a number, sitting in a String.
§III — Result you must use
read_line returns a Result. Result is an enum. Variants: Ok and Err. Ok holds the success value. For read_line that value is the byte count. Err holds why it failed, usually from the operating system.
.expect("Failed to read line") is the book's crash. If the Result is Err, the program panics with that message. If it is Ok, expect unwraps the inner value and you ignore the byte count.
Leave expect off and cargo build still succeeds. You get a warning:
warning: unused `Result` that must be used
--> src/main.rs:10:5
|
10 | io::stdin().read_line(&mut guess);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
Rust will not let a Result fall on the floor without a note. let _ = ... silences it. That is not handling. expect is the tutorial's crash. Chapter 9 is recovery. You will replace one expect later in this chapter. You will keep the other.
The long form of the call is one logical line. The book splits it across three so the method chain can be read. Either form returns the same Result.
§IV — rand is not in std
The secret must change each run. Rust's standard library has no random number generator in this edition of the book. The Rust team publishes the rand crate. Your crate is a binary crate: it runs. rand is a library crate: it is used.
Open Cargo.toml. Under [dependencies] write exactly what the chapter writes:
[dependencies]
rand = "0.8.5"
The book pins 0.8.5. That specifier is ^0.8.5: at least 0.8.5, below 0.9.0. Cargo talks SemVer. Do not write 0.9 because a build log said available: v0.9.0. The listings in this chapter call thread_rng and gen_range. Those names are the 0.8 API.
cargo build after that line talks to crates.io, locks a tree of crates rand needs, and compiles them. Listing 2-2 in the book shows fifteen packages locking, then rand v0.8.5. Your versions will differ in patch. The public API of 0.8.x is the contract.
Cargo.lock appears on the first build. Cargo writes the exact versions it chose. Later builds reuse that file. Check it in with the source if this crate is shared. cargo update ignores the lock, still honors the Cargo.toml range, and rewrites the lock. It will take 0.8.6. It will refuse 0.9.
cargo build a second time with no edits prints Finished and stops. Edit src/main.rs and only your crate recompiles. The downloaded crates stay.
Listing 2-3 uses the crate:
use rand::Rng;
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
Rng is a trait. It must be in scope for gen_range to resolve. Chapter 10 is traits. thread_rng gives a generator local to this thread, seeded by the OS. 1..=100 is inclusive on both ends.
cargo doc --open, then click rand in the sidebar, is how you read a crate you did not write.
The secret-print is a test probe. Delete it before you call the game done. Two runs should print two different secrets in 1 through 100.
§V — the compare, then the type error
Bring Ordering in. Call cmp. match the three variants.
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
Ordering is an enum: Less, Greater, Equal. cmp takes a reference to the other value. match walks arms in order and runs the first that fits. It stops there.
This listing does not compile. The book shows the error on purpose.
error[E0308]: mismatched types
--> src/main.rs:23:21
|
23 | match guess.cmp(&secret_number) {
| --- ^^^^^^^^^^^^^^ expected `&String`, found `&{integer}`
guess is a String. secret_number is a number. Rust inferred i32 for the secret because nothing asked for another width. cmp will not compare a string to an integer.
That error is the lesson wearing a compiler code. You have been holding the guess that is not a number. Shadow it.
let guess: u32 = guess.trim().parse().expect("Please type a number!");
The new guess reuses the name. The right-hand guess is still the String. trim drops the newline (\n, or \r\n on Windows) and any spaces. After read_line, a typed 5 is 5\n. parse reads digits. The : u32 annotation tells parse which number. u32 is an unsigned 32-bit integer. It is the book's default for a small positive count. The same annotation pulls secret_number to u32, so cmp now sees two of one type.
parse returns Result. Non-digits (A👍%) are Err. The book's first pass crashes with expect("Please type a number!"). Same shape as read_line. Two Results, two crashes, one chapter.
Run a high guess, a low guess, and a hit. Spaces before the digits still parse. Then you get one shot and the process ends.
§VI — loop, break, continue
loop is infinite. Move the prompt, the String, read_line, the parse, the print, and the match inside it. Indent four more spaces. The game now asks forever. Ctrl-C stops it. A non-number also stops it, because expect on parse panics.
On Ordering::Equal, print You win! and break. Leaving the loop leaves main. That is the win path.
Replace the parse expect with a match. Listing 2-5:
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
Ok(num) binds the integer and that becomes the new guess. Err(_) matches every parse failure. continue starts the next iteration and asks again. foo is no longer a panic. It is a skipped turn.
Delete the secret println!. Listing 2-6 is the finished program.
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
Two Results remain. read_line still expects; an OS-level stdin failure is rare and fatal here. parse now matches; a bad guess is common and recoverable. That split is the chapter's last teaching. Chapter 9 will name it as policy.
§VII — Four proofs
The syllabus names one walk with four parts. Run them.
Proof 1. Args of a sort.
The guess arrives through io::stdin().read_line(&mut guess), not through std::env::args. If your walk starts at argv, you are in the wrong chapter. If guess is not mut and the borrow is not &mut, read_line cannot append.
**Proof 2. Result.**
Name both. read_line returns Result. Unused, it warns. expect crashes. parse returns Result. expect crashes. match with Err(_) => continue keeps the loop. If you cannot say which Result still panics in Listing 2-6, walk §III and §VI again.
**Proof 3. crates.io rand.**
rand = "0.8.5" under [dependencies]. use rand::Rng;. rand::thread_rng().gen_range(1..=100). Cargo.lock pins the tree. cargo doc --open is the local API. If you called something in std for the secret, you left the book.
Proof 4. A test of the compare.
guess.cmp(&secret_number) returns Ordering. match has three arms: Less, Greater, Equal. Equal breaks. A String compared to an integer is E0308. The shadow to u32 is what makes the compare legal. If you wrote == and two ifs, you skipped the enum the chapter came to show.
Play it once with the secret print still in, once with it gone. Type foo in the finished loop. The prompt should return. Then hit the number.
§VIII — Closing
Ch.2 is one crate and four names. stdin fills a String. Result refuses to be ignored. rand arrives through Cargo.toml. Ordering is the compare.
Name it when cmp asks for &String and you offered a number: the guess that is not a number. Trim. Parse. Shadow. Then match.
Session 03 is TRPL Ch.3, common programming concepts. Mutability, types, functions, control flow, from the book. Do not start it in this folder. Do not write it today.
Examine well. The stdin line is the first proof. The two Results are the second. The rand pin is the third. The Ordering match is the fourth. The function name main will still be pretty. The door is the parse.
Related
- Prior fire: Duha session 01, the file that is not a crate
- Syllabus: Duha Rust syllabus, session 02
- Grounding tome: TRPL Ch.2 Guessing Game (Klabnik / Nichols, stable)
- Next fire: session 03, TRPL Ch.3 Common Programming Concepts (unwritten)