No history yet

Building a Simple Rust Application

Putting It All Together

It's time to apply everything you've learned. We'll build a classic command-line program: a number guessing game. This project will touch on variables, data types, functions, control flow, ownership, and error handling. It's a great way to see how Rust's core concepts work together in a real application.

First, let's create a new project using Cargo. Open your terminal and run this command:

cargo new guessing_game

Cargo generates a new project directory called guessing_game. Inside, you'll find a src folder with a main.rs file and a Cargo.toml file for managing dependencies. Navigate into the new directory:

cd guessing_game

Let's open src/main.rs. We'll start by getting input from the user.

Handling User Input

To read what the user types, we need to bring the input/output functionality from Rust's standard library into scope. We'll also create a mutable string variable to store the user's guess.

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}");
}

Let's break this down:

  • use std::io; brings the io module into scope.
  • let mut guess = String::new(); creates an empty, mutable string. Remember, variables are immutable by default in Rust.
  • io::stdin().read_line(&mut guess) reads the user's input and appends it to our guess string. We pass a mutable reference (&mut guess) so read_line can change the string's content.
  • .read_line() returns a Result type. We use the .expect() method as a simple way to handle the Err variant, which would crash the program and display our message if something went wrong.

Run the program with cargo run. It will wait for your input, and then print it back to you.

Generating a Secret Number

We need a secret number for the user to guess. For this, we'll use a crate—Rust's term for a package of code. The rand crate provides random number generation. First, we need to add it as a dependency in our Cargo.toml file. Open it and add the following line under [dependencies]:

[dependencies]
rand = "0.8.5"

Now, let's update src/main.rs to generate a random number between 1 and 100.

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}");
}

Here's what's new:

  • use rand::Rng; imports the Rng trait, which provides the methods for random number generation.
  • rand::thread_rng() gives us a random number generator that's local to the current thread of execution.
  • gen_range(1..=100) generates a random number within the range of 1 to 100, inclusive.

The first time you run this with cargo run, Cargo will download and compile the rand crate. For now, we're printing the secret number to make testing easier.

Comparing and Looping

Now we need to compare the user's guess with the secret number. The user input, guess, is a String, while secret_number is an integer. We can't compare them directly. We must first convert the string to a number.

let guess: u32 = guess.trim().parse().expect("Please type a number!");

This line shadows the previous guess variable. The .trim() method removes any whitespace, and .parse() converts the string to a number. We specify the type u32 for our new guess variable. Like read_line, parse returns a Result, so we use expect to handle potential errors, like if the user types "cat" instead of a number.

Next, we'll use a match expression to compare the guess to the secret number and a loop to let the user keep guessing until they get it right.

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..=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 = 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;
            }
        }
    }
}

The cmp method compares two values and returns a variant of the Ordering enum: Less, Greater, or Equal. The match statement is the perfect way to handle these different outcomes.

When the guess is correct (Ordering::Equal), we print a success message and use break to exit the loop. Once you're confident it's working, you can remove the line that prints the secret_number.

Finally, what if the user enters something that isn't a number? Our program panics. Let's handle that gracefully by ignoring invalid input and letting the user try again.

let guess: u32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => continue,
};

Instead of calling expect, we switch to a match expression on the Result from parse. If it's an Ok variant, we return the number. If it's an Err, we use continue to skip to the next iteration of the loop, ignoring the invalid input.

Here is the complete, final program:

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..=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;
            }
        }
    }
}

Congratulations! You've built a complete Rust application from scratch, tying together many of the language's core features in a practical way.

Quiz Questions 1/6

In a Rust project managed by Cargo, where do you declare an external dependency like the rand crate so Cargo will download and compile it?

Quiz Questions 2/6

Consider the following code: let mut guess = String::new(); io::stdin().read_line(&mut guess).expect("Failed to read line");. Why must guess be declared as mutable (mut)?