Notes (TRPL 02/21): Programming a Guessing Game
2 Programming a Guessing Game
Setting up a New Project
Despite having gone through all the work to figure out carnix
in the last chapter, I think I’m just going to use cargo
without nix
for the moment, so that I can learn the usual non-nix
way first. I kinda wish cargo automatically integrated with (despite the fact that nix
like stack
does with haskell, so I don’t have to throught the extra cargo build
, carnix
and nix-build
steps.carnix
is more ergonomic now, it’s still bleeding-edge undocumented code, so i don’t want to run into issues that might complicate my rust learning.)
Handling Potential Failure with the Result
Type
How can we make read_line
fail though? The docs say that it errors on any bytes that aren’t valid UTF-8, but I tried typing in some various malformed UTF-8 sequences (with <C-U>
) from this UTF-8 test page, and couldn’t get it to error.
I will keep investigating.
Generating a Random Number
The cargo doc --open
command is awesome.
Comparing the Guess to the Secret Number
Okay, great we have pattern matching, excellent.
I’m not sure how I feel about the .function
notation for doing function “piping”. It’s very terse and legible, but I guess I can’t help but compare it to Haskell does make me appreciate how >>=
is just a regular function and not special language syntax.
I’m going to try to dampen that “But in Haskell…” reflex though, this is a different language with its own philosophy of programming, particularly on how best to safely handle mutable state.
Allowing Multiple Guesses with Looping
To expand on the previous thought, I think there’s actually great value in the way Rust provides concise conceptual tools for dealing with control flow and state mutation. Take the continue
keyword in this example, for instance. If we were trying to accomplish the same thing with pure code, we’d probably assign a name to the loop and use recursion:
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1, 101);
println!("The secret number is: {}", secret_number);
;
loop_(secret_number)
}
fn loop_(secret : u32) -> u32 {
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(_) => loop_(secret),
};
println!("You guessed: {}", guess);
match guess.cmp(&secret) {
Ordering::Less => {println!("Too small!"); loop_(secret) }
Ordering::Greater => { println!("Too big!"); loop_(secret) }
Ordering::Equal => { println!("You win!"); return guess; }
}
}
But this actually doesn’t work! If the parsing fails, the pattern matching continuation doubles up on itself. So we actually have to do:
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1, 101);
println!("The secret number is: {}", secret_number);
;
loop_(secret_number)
}
fn loop_(secret : u32) -> u32 {
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) => continue_(secret, num),
Err(_) => loop_(secret),
};
return guess;
}
fn continue_(secret : u32, guess : u32) -> u32 {
println!("You guessed: {}", guess);
match guess.cmp(&secret) {
Ordering::Less => {println!("Too small!"); loop_(secret) }
Ordering::Greater => { println!("Too big!"); loop_(secret) }
Ordering::Equal => { println!("You win!"); return guess; }
}
}
This is pure, but ugly. As opposed to the example from the text, which is impure yet lovely (“O daughters of Jerusalem”).
The pure code has a lot of plumbing, and okay a language designed for that plumbing is going to be little cleaner, but only a little, and for differet reasons.
I mean, I know I said was going to tone down the Haskell comparisons, but this is actually a point in Rust’s favo, so look at what a Haskell verison of the guessing game might look like:
module Main where
import System.Random
import Text.Read
main :: IO ()
= do
main <- getStdRandom (randomR (1,101))
secret putStrLn "secret is: "
print secret
let loop = do
putStrLn "Please input your guess"
<- getLine
guess case (readMaybe guess) :: Maybe Int of
Nothing -> loop
Just x -> case (compare guess secret) of
GT -> (putStrLn "Too high" >> loop)
EQ -> (putStrLn "You win!" >> return ())
LT -> (putStrLn "Too low" >> loop)
loop
Is this more concise? Definitely more consise than the pure Rust guessing game, but it’s actually about comparable to the idiomatic main.rs
in the book (23 lines for Haskell vs 25 or 33 for Rust depending on if count the empty lines in the function body).
But the thing is that the Rust code exposes a lot of information that the Haskell code abstracts over! For example, the guess
in my Haskell code should actually be using IORef
and Text
to better match the fact that the Rust code creates an empty mutable string variable. And that’s not even getting into the borrowing and reference management that Rust is doing.
In this particular example it doesn’t matter, but I can imagine a lot of cases where the fact that Rust exposes a lot of granular low-level detail might come in really helpful.