36 KiB
[TOC]
Guessing Game
Let's jump into Rust with a hands-on project! This chapter will introduce you to
a few common Rust concepts by showing how you would use them in a real program.
You'll learn about let
, match
, methods, associated functions, using
external crates, and more! Following chapters will explore these ideas in more
detail.
We’re going to implement a classic beginner programming problem: the guessing game. Here’s how it works: Our program will generate a random integer between one and a hundred. It will then prompt us to enter a guess. Upon entering our guess, it will tell us if we’re too low or too high. Once we guess correctly, it will congratulate us.
Setting Up a New Project
Let’s set up a new project. Go to your projects directory from the previous chapter, and create a new project using Cargo, like so:
$ cargo new guessing_game --bin
$ cd guessing_game
We pass the name of our project to cargo new
and pass the --bin
flag, since
we’re going to be making another binary like in Chapter 1.
Take a look at the generated Cargo.toml
:
Filename: Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
[dependencies]
If the author information that Cargo got from your environment is not correct, go ahead and fix that in the file and save it again.
And as we saw in the last chapter, cargo new
generates a "Hello, world!"
program for us. Check out src/main.rs
:
Filename: src/main.rs
fn main() {
println!("Hello, world!");
}
Let’s try compiling what Cargo gave us and running it in the same step, using
the cargo run
command:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Running `target/debug/guessing_game`
Hello, world!
Great! The run
command comes in handy when you need to rapidly iterate on a
project. Our game is such a project: we want to quickly test each
iteration before moving on to the next one.
Now open up your src/main.rs
again. We’ll be writing all of our code in this
file.
Processing a Guess
Let’s get to it! We'll split the development of this game up into parts. This
first part will ask for input from a user and process the input, checking that
the input is in the form we expect. First we need to allow our player to input
a guess. Enter this in your src/main.rs
:
Filename: src/main.rs
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);
}
There’s a lot here! Let’s go over it, bit by bit.
use std::io;
We’ll need to take user input and then print the result as output, and for that
functonality we need to import the io
(input/output) library from the
standard library (which is known as std
).
By default, Rust only imports a few things into every program in the
prelude. If it’s not in the prelude, you’ll have to import it into
your program explicitly with a use
statement. Using the std::io
library
gets you a number of useful io
-related things, including the functionality to
accept user input.
fn main() {
As you’ve seen in Chapter 1, the main()
function is the entry point into the
program. The fn
syntax declares a new function, the ()
s indicate that
there are no arguments, and {
starts the body of the function.
println!("Guess the number!");
println!("Please input your guess.");
As we learned in Chapter 1, println!()
is a macro that prints a string to the
screen. This is just a prompt stating what the game is and requesting input from
the user.
Storing Values with Variable Bindings
Next we need to store the user input.
let mut guess = String::new();
Now we’re getting interesting! There’s a lot going on in this little line.
The first thing to notice is that this is a let
statement, which is
used to create variable bindings. Here's another example:
let foo = bar;
This will create a new binding named foo
, and bind it to the value bar
. In
many languages, this is called a variable, but Rust’s variable bindings have
a few differences.
For example, they’re immutable by default. To make our binding mutable, our
example uses mut
before the binding name.
let foo = 5; // immutable.
let mut bar = 5; // mutable
Note: The
//
syntax will start a comment that continues until the end of the line. Rust ignores everything in comments.
So now we know that let mut guess
will introduce a mutable binding named
guess
, but we have to look at the other side of the =
for the value it’s
bound to: String::new()
.
String
is a string type, provided by the standard library. A
String
is a growable, UTF-8 encoded bit of text.
The ::
syntax in the ::new()
line indicates that new()
is an associated
function of a particular type. An associated function is a function that is
associated with a type, in this case String
, rather than a particular
instance of a String
. Some languages call this a static method.
This new()
function creates a new, empty String
.
You’ll find a new()
function on many types, as it’s a common name for making
a new value of some kind.
So to summarize, the let mut guess = String::new();
line has created a
mutable binding that is currently bound to a new, empty instance of a String
.
Whew!
Let’s move forward:
io::stdin().read_line(&mut guess)
.expect("Failed to read line");
Remember how we said use std::io;
on the first line of the program? We’re now
calling an associated function on it. If we didn’t use std::io
, we could
have written this line as std::io::stdin()
.
This particular function returns an instance of std::io::Stdin
,
which is a type that represents a handle to the standard input for your
terminal.
The next part, .read_line(&mut guess)
, calls the readline()
method on the standard input handle to get input from the user. We’re also
passing one argument to read_line()
: &mut guess
.
The job of read_line()
is to take whatever the user types into standard input
and place that into a string, so it takes that string as an argument. The
string argument needs to be mutable so that the method can change the string's
content by adding the user input.
The &
indicates that this argument is a reference, which gives you a way to
allow multiple parts of your code to access to one piece of data without
needing to copy that data into memory multiple times. References are a complex
feature, and one of Rust’s major advantages is how safe and easy it is to use
references. We don’t need to know a lot of those details to finish our program
right now, though; Chapter XX will cover references in more detail. For now,
all we need to know is that like let
bindings, references are immutable by
default. Hence, we need to write &mut guess
, rather than &guess
to make it
mutable.
We’re not quite done with this line of code. While it’s a single line of text, it’s only the first part of the single logical line of code. The second part is this method:
.expect("Failed to read line");
When you call a method with the .foo()
syntax, it's often wise to introduce a
newline and other whitespace. This helps you split up long lines. We could
have written this code as:
io::stdin().read_line(&mut guess).expect("failed to read line");
But that gets hard to read. So we’ve split it up, two lines for two method calls. Now let's see what this line does.
Handling Potential Failure with the Result
Type
We mentioned that read_line()
puts what the user types into the string we
pass it, but it also returns a value: in this case, an io::Result
. Rust has a
number of types named Result
in its standard library: a generic Result
, and
then specific versions for sub-libraries, like io::Result
.
The Result
types are enums, which is short for enumeration. An
enumeration is a type that can have a fixed set of values, which are called the
enum
's variants. We will be covering enums in more detail in Chapter XX.
For Result
, the variants are Ok
or Err
. Ok
means the operation was
successful, and inside the Ok
variant is the successfully generated value.
Err
means the operation failed, and the Err
contains information about how
or why the operation failed.
The purpose of these Result
types is to encode error handling information.
Values of the Result
type, like any type, have methods defined on them. In
this case, io::Result
has an expect()
method that we can call. If
this instance of io::Result
is an Err
value, expect()
will cause our
program to crash and display the message that we passed as an argument to
expect()
. In this case, if the read_line()
method returns an Err
, it would
likely be the result of an error coming from the underlying operating system.
If this instance of io::Result
is an Ok
value, expect()
will take the
return value that Ok
is holding out of the Ok
and return just that value to
us so that we can use it. In this case, that value will be what the user
entered into standard input.
If we don't call expect()
, our program will compile, but we’ll get a warning:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
src/main.rs:10:5: 10:39 warning: unused result which must be used,
#[warn(unused_must_use)] on by default
src/main.rs:10 io::stdin().read_line(&mut guess);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Rust warns that we haven’t used the Result
value, telling us that we
haven’t handled a possible error. The right way to suppress the warning is to
actually write error handling, but if we just want to crash the program when a
problem occurs, we can use expect()
. We’ll save recovering from errors for a
future project.
Printing Values with println!()
Placeholders
There’s only one line of this first example left, aside from the closing curly brace:
println!("You guessed: {}", guess);
This prints out the string we saved our input in. The {}
s are a placeholder:
think of {}
as little crab pincers, holding a value in place. You can print
more than one value this way: the first {}
holds the first value listed after
the format string, the second set holds the second value, and so on. Printing
out multiple values in one call to println!()
would then look like this:
let x = 5;
let y = 10;
println!("x = {} and y = {}", x, y);
Which would print out "x = 5 and y = 10".
Testing the First Part
Back to our guessing game, let's test what we have so far. We can run it with
cargo run
:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6
All right! Our first part is done: we can get input from the keyboard and then print it back out.
Generating a Secret Number
Next, we need to generate a secret number that the user is trying to guess. The
secret number should be different every time so that the game is fun to play
more than once. So we'd like to have a random number between 1 and 100. Rust
does not yet include random number functionality in its standard library. The
Rust team does, however, provide a rand
crate at https://crates.io/crates/rand.
Using a Crate to Get More Functionality
Remember that crate is what we call a package of Rust code. The project we’ve
been building is a binary crate, which is an executable. The rand
crate is
a library crate, which contains code intended to be used in other programs.
Cargo's use of external crates is where it really shines. Before we can write
the code using rand
, we need to modify our Cargo.toml
to include the rand
crate as a dependency. Open it up, and add this line at the bottom beneath the
[dependencies]
section header that Cargo created for you:
Filename: Cargo.toml
[dependencies]
rand = "0.3.14"
In the Cargo.toml
file, everything that follows a header is part of a section
that goes until another section starts. Cargo uses the [dependencies]
section
to know what external crates your project depends on and what versions of those
crates you require. In this case, we’ve specified the rand
crate with the
semantic version specifier 0.3.14
. Cargo understands Semantic
Versioning (sometimes called semver), which is a standard for
writing version numbers. A bare number like above is actually shorthand for
^0.3.14
, which means "any version that has a public API compatible with
version 0.3.14".
Now, without changing any of our code, let’s build our project:
$ cargo build
Updating registry `https://github.com/rust-lang/crates.io-index`
Downloading rand v0.3.14
Downloading libc v0.2.14
Compiling libc v0.2.14
Compiling rand v0.3.14
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
You may see different version numbers (but they will all be compatible with your code, thanks to semver!) and the lines may be in a different order.
Lots of new output! Now that we have an external dependency, Cargo fetches the latest versions of everything from the registry, which is a copy of data from https://crates.io. Crates.io is where people in the Rust ecosystem post their open source Rust projects for others to use.
After updating the registry, Cargo checks our [dependencies]
and downloads
any we don’t have yet. In this case, while we only listed rand
as a
dependency, we’ve also grabbed a copy of libc
, because rand
depends on
libc
to work. After downloading them, Rust compiles them and then compiles
our project.
If we run cargo build
again, we’ll get different output:
$ cargo build
That’s right, no output! Cargo knows that our project has been built, that
all of its dependencies are built, and that no changes have been made. There’s
no reason to do all that stuff again. With nothing to do, it simply
exits. If we open up src/main.rs
, make a trivial change, then save it again,
we’ll only see one line:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
This just updates the build with your tiny change to the main.rs
file.
The Cargo.lock File that Ensures Reproducible Builds
Cargo has a mechanism to make sure we can rebuild the exact same artifact every
time we or anyone else builds our code: Cargo will only use the versions of the
dependencies you specified until you say otherwise. For example, what happens
if next week version v0.3.15
of the rand
crate comes out, containing an
important bugfix, but that also contains a regression that will break our code?
The answer to this problem is the Cargo.lock
file created the first time we
ran cargo build
that is now in your project directory. When you build your
project for the first time, Cargo figures out all of the versions of your
dependencies that fit your criteria then writes them to the Cargo.lock
file.
When you build your project in the future, Cargo will see that the Cargo.lock
file exists and use the versions specified there rather than doing all the work
of figuring out versions again. This lets you have a reproducible build
automatically. In other words, our project will stay at 0.3.14
until we
explicitly upgrade, thanks to the lock file.
Updating a Crate to Get a New Version
When we do want to update a crate, Cargo has another command,
update
, which will:
- Ignore the
Cargo.lock
file and figure out all the latest versions that fit our specifications inCargo.toml
. - If that works, write those versions out to the lock file.
But by default, Cargo will only look for versions larger than 0.3.0
and
smaller than 0.4.0
. If the rand
crate has released two new versions,
0.3.15
and 0.4.0
, this is what we would see if we ran cargo update
:
$ cargo update
Updating registry `https://github.com/rust-lang/crates.io-index`
Updating rand v0.3.14 -> v0.3.15
At this point, you would also notice a change in your Cargo.lock
file noting
that the version of the rand
crate you are now using is 0.3.15
.
If we wanted to use rand
version 0.4.0
or any version in the 0.4.x
series, we’d have to update what is in the Cargo.toml
file to look like this
instead:
[dependencies]
rand = "0.4.0"
The next time we cargo build
, assuming that the rand
crate version 0.4.0
has been released, Cargo will update the crates index and re-evaluate our
rand
requirements according to the new version we have specified.
There’s a lot more to say about Cargo and its ecosystem that we will get into in Chapter XX, but for now, that’s all we need to know. Cargo makes it really easy to re-use libraries, so Rustaceans are able to write smaller projects which are assembled out of a number of sub-packages.
Generating a Random Number
Let’s get on to actually using rand
. Our next step is to update our
main.rs
code as follows:
Filename: src/main.rs
extern crate rand;
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1, 101);
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);
}
First we've added a line to the top, extern crate rand
, that lets Rust know
we’ll be making use of that external dependency. This also does the equivalent
of calling use rand
, so we can now call anything in the rand
crate by
prefixing it with rand::
.
Next, we added another use
line: use rand::Rng
. Rng
is a trait that
defines methods that random number generators implement, and this trait must be
in scope for us to use those methods. We'll cover traits in detail in the
Traits section in Chapter XX.
We also added two more lines in the middle:
let secret_number = rand::thread_rng().gen_range(1, 101);
println!("The secret number is: {}", secret_number);
rand::thread_rng()
is a function that will give us the particular random
number generator that we're going to use: one that is local to our current
thread of execution and seeded by the operating system. Next, we call the
gen_range()
method on our random number generator. This method is one that is
defined by the Rng trait that we brought into scope with the use rand::Rng
statement above. gen_range()
takes two numbers as arguments and generates a
random number between them. It’s inclusive on the lower bound but exclusive on
the upper bound, so we need 1
and 101
to ask for a number ranging from one
to a hundred.
Knowing what traits to import and what functions and methods to use from a
crate isn't something that you'll just know. Instructions for using a crate
are in each crate's documentation. Another neat feature of Cargo is that you
can run the cargo doc --open
command to build documentation provided by all
of your dependencies locally and then open it in your browser. If you're
interested in other functionality in the rand
crate, for example, run cargo doc --open
then click on "rand" in the sidebar on the left.
The second line that we added to our code prints out the secret number. This is useful while we’re developing our program to let us easily test it out, but we’ll be deleting it for the final version. It’s not much of a game if it prints out the answer when you start it up!
Try running our new program a few times:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4
$ cargo run
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5
You should get different random numbers, and they should all be numbers between 1 and 100. Great job!
Comparing Our Guesses
Now that we’ve got user input, let’s compare our guess to the secret number. Here’s that part of our next step:
Filename: src/main.rs
extern crate rand;
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1, 101);
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!"),
}
}
There are a few new bits here. The first is another use
, bringing a type
called std::cmp::Ordering
into scope from the standard crate. Ordering
is
another enum, like Result
, but the variants for Ordering
are Less
,
Greater
, and Equal
. These are the three outcomes that are possible when you
compare two things.
Then we add five new lines at the bottom that use the Ordering
type:
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
The cmp()
method compares two values, and can be called on anything that can
be compared. It takes a reference to the thing you want to compare it to, so
here it's comparing our guess
to our secret_number
. cmp()
returns a
variant of the Ordering
type we imported with the use
statement earlier. We
use a match
statement to decide what to do next based on which
variant of Ordering
we got back from our call to cmp()
with the values in
guess
and secret_number
.
match
statements are made up of arms. An arm consists of a pattern and
the code that we should run if the value given to the beginning of the match
statement fits that arm's pattern. Rust takes the value given to match
and
looks through each arm's pattern in turn. The match
construct and patterns
are powerful features in Rust that will be covered in detail in Chapter XX and
Chapter XX, respectively.
Let's walk through an example of what would happen with our match
. Say that
the user has guessed 50, and the randomly-generated secret number this time
is 38. So when we compare 50 to 38, the cmp()
method will return
Ordering::Greater
, since 50 is greater than 38. Ordering::Greater
is the
value that the match
statement gets. It looks at the first arm's pattern,
Ordering::Less
, and says nope, the value we have (Ordering::Greater
) does
not match Ordering::Less
. So it ignores the code in that arm and moves on to
the next arm. The next arm's pattern, Ordering::Greater
, does match
Ordering::Greater
! The associated code in that arm will get executed, which
prints "Too big!" to the screen. Then we're done with the match
statement; we
don't look at the last arm at all in this particular scenario.
However, this code won’t quite compile yet. Let’s try it:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
src/main.rs:23:21: 23:35 error: mismatched types [E0308]
src/main.rs:23 match guess.cmp(&secret_number) {
^~~~~~~~~~~~~~
src/main.rs:23:21: 23:35 help: run `rustc --explain E0308` to see a detailed explanation
src/main.rs:23:21: 23:35 note: expected type `&std::string::String`
src/main.rs:23:21: 23:35 note: found type `&_`
error: aborting due to previous error
Could not compile `guessing_game`.
Whew! This is a big error. The core of the error says that we have mismatched
types. Rust has a strong, static type system. However, it also has type
inference. When we wrote let guess = String::new()
, Rust was able to infer
that guess
should be a String
and didn’t make us write the type out. Our
secret_number
on the other hand is a number type. There are a few number
types which can have a value between one and a hundred: i32
, a thirty-two-bit
number; or u32
, an unsigned thirty-two-bit number; i64
, a sixty-four-bit
number; or others. Rust defaults to an i32
, so that's the type of
secret_number
. The error is because Rust will not compare two different types.
Ultimately, we want to convert the String
we read as input
into a real number type so that we can compare it to the guess numerically. We
can do that with two more lines; add this to your program:
Filename: src/main.rs
extern crate rand;
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1, 101);
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");
let guess: u32 = guess.trim().parse()
.expect("Please type a number!");
println!("You guessed: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
The two new lines are:
let guess: u32 = guess.trim().parse()
.expect("Please type a number!");
We create a variable binding guess
. But wait a minute, don't we already have
a variable binding named guess
? We do, but Rust allows us to shadow the
previous value of guess
with a new one. This is often used in this exact
situation, where we want to convert a value from one type into another type.
Shadowing lets us re-use the guess
variable name rather than forcing us to
come up with two unique bindings, like guess_str
and guess
or something.
We bind guess
to the expression guess.trim().parse()
.
The guess
in the expression refers to the original guess
that was a
String
with our input in it. The trim()
method on String
s will eliminate
any white space at the beginning and end. Our u32 can only contain numerical
characters, but we have to press the "return" key to satisfy read_line()
.
When we press the return key, it introduces a newline character. For example,
if we type 5
and hit return, guess
looks like this: 5\n
. The \n
represents "newline", the return key. The trim()
method gets rid of this,
leaving our string with only the 5
.
The parse()
method on strings parses a string into some kind of
number. Since this method can parse a variety of number types, we need to tell
Rust the exact type of number we want with let guess: u32
. The colon (:
)
after guess
tells Rust we’re going to annotate its type. Rust has a few
built-in number types, but we’ve chosen u32
, an unsigned, thirty-two bit
integer. It’s a good default choice for a small positive number. You'll see the
other number types in Chapter XX.
Our call to parse()
could quite easily cause an error, if, for example, our
string contained A👍%
; there’d be no way to convert that to a number. Because
it might fail, the parse()
method returns a Result
type, much like the
read_line()
method does that we discussed earlier. We're going to treat this
Result
the same way by using the expect()
method again. If parse()
returns an Err
Result
variant because it could not create a number from the
string, the expect()
call will crash the game and print the message we give
it. If parse()
can successfully turn the string into a number, it will return
the Ok
variant of Result
, and expect()
will return the number that we
want that it will take out of the Ok
value for us.
Let’s try our program out!
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Running `target/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
76
You guessed: 76
Too big!
Nice! You can see we even added spaces before our guess, and it still figured out that we guessed 76. Run the program a few times to verify the different behavior with different kinds of input: guess the number correctly, guess a number that is too high, and guess a number that is too low.
Now we’ve got most of the game working, but we can only make one guess. Let’s change that by adding a loop!
Allowing Multiple Guesses with Looping
The loop
keyword gives us an infinite loop. We'll add that in to give us more
chances at guessing the number:
Filename: src/main.rs
extern crate rand;
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1, 101);
println!("The secret number is: {}", secret_number);
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 = guess.trim().parse()
.expect("Please type a number!");
println!("You guessed: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
}
As you can see, we've moved everything from the guess input onwards into a loop. Make sure to indent those lines another four spaces each, and try it out. You'll notice we have a new problem because the program is doing exactly what we told it to do: ask for another guess forever! It doesn't seem like we can quit!
We could always halt the program by using the keyboard shortcut control-c
.
There's another way to escape the monster we've created that will infinitely
demand more guesses, though, that can be found in our discussion about
parse()
: if we give a non-number answer, the program will crash. We can use
that to quit! Observe:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Running `target/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit
thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidDigit }', src/libcore/result.rs:785
note: Run with `RUST_BACKTRACE=1` for a backtrace.
error: Process didn't exit successfully: `target/debug/guess` (exit code: 101)
This method means that typing quit
actually quits the game, but so does any
other non-number input. This is suboptimal, to say the least. We want the game
to automatically stop when the correct number is guessed.
Quitting When you Win
Let’s program the game to quit when you win by adding a break
in that case:
Filename: src/main.rs
extern crate rand;
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1, 101);
println!("The secret number is: {}", secret_number);
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 = guess.trim().parse()
.expect("Please type a number!");
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;
}
}
}
}
By adding the break
line after You win!
, we’ll exit the loop when we guses
the secret number correctly. Exiting the loop also means exiting the program,
since the loop is the last thing in main()
.
Handling Invalid Input
For our final refinement of the game's behavior, rather than crashing the
program when someone inputs a non-number, we want the game to ignore it so we
can continue guessing. We can do that by altering the line where we convert
guess
from a String
to a u32
:
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
This is how you generally move from "crash on error" to "actually handle the
error": by switching from an expect()
statement to a match
statement.
Remember that parse()
returns a Result
type, and Result
is an enum that
has the variants Ok
or Err
. We're going to use a match
statement here,
like we did with the Ordering
result of the cmp()
method.
If parse()
is able to successfully turn the string into a number, it will
return an Ok
value that contains the resulting number. That Ok
value will
match the first arm's pattern, and the match statement will just return the
num
value that parse()
produced and put inside the Ok
value. That number
will end up right where we want it, in the new guess
binding we're creating.
If parse()
is not able to turn the string into a number, it will return an
Err
value that contains more information about the error. The Err
value
does not match the Ok(num)
pattern in the first match arm, but it does match
the Err(_)
pattern in the second arm. The _
is a catch-all value; we're
saying we want to match all Err
values, no matter what information they have
inside them. So we execute the second arm's code, continue
: this means to go
to the next iteration of the loop
and ask for another guess. So we have
effectively ignored all errors that parse()
might hit!
Now everything in our program should work as we expect it to! Let’s try it:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Running `target/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!
Awesome! With one tiny last tweak, we can finish the guessing game: we're still
printing out the secret number. That was good for testing, but it kind of ruins
the game. Let's delete the println!
that outputs the secret number. Here’s our
full, final code:
Filename: src/main.rs
extern crate rand;
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1, 101);
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;
}
}
}
}
Complete!
At this point, you have successfully built the Guessing Game! Congratulations!
This project was a hands-on way to introduce you to a lot of new Rust concepts:
let
, match
, methods, associated functions, using external crates, and more.
In the next few chapters, we will go through these concepts in more detail.
Chapter 3 covers concepts that most programming languages have, like variables,
data types, and functions, and shows how to use them in Rust. Chapter 4 gets
into ownership, which is the feature of Rust that is most different from other
languages. Chapter 5 discusses structs and method syntax, and Chapter 6
endeavors to explain enums.