Rust - Part 2

Written on
This article is for those who already have some experience with rust, and want to refresh the concepts.
All the articles from this series are condensed versions of Rust official book, this post covers Chapter 2 from the book.

Chapter 2:

1.  Setup:

Let's create a number guessing game.  Let's create a new package and add some print statements.

cargo new guessing_game

cd guessing_game

use std::io;

fn main() {
    println!("Guess the number");
    
    println!("Your guess?");
    let mut guess = String::new();
    io::stdin()
        .read_line(&mut guess)
        .expect("Failed");

    println!("You guessed: {}", guess);
}

use std::io is like importing packages in java

println! is a macro, we'll get to that later.

In rust values are immutable by default, so we need to add the mut before declaration

:: is equivalent to dot operator in other languages to call functions, but only "static" like functions.  If we are going to be calling it an object, instead of the String type itself, then we can use the dot operator.

 

io::stdin() means we are calling stdin method of io module, and again similar to other languages, if we don't add the use std::io in the first line, we can still call the stdin method, but this time we have to call it like std::io::stdin()

read_line takes a String reference as an input, and appends the standard input to the variable.

I would've guessed that the input to pass would be &guess, but instead we'll be passing &mut guess (The book says that this will be explained in the later part).

The return type is of io::Result, which is nothing but an enum with different variants - as far as Result goes, it's just Ok and Err.

If the result of read_line is Err, program will crash and display the error message we have passed it as an argument, but if the result is Ok, expect will take the value that Ok is holding and return just the value.

If we skip the expect part, the program will be compiled, but we will get a warning.

Print line is self explanatory, you can also add multiple arguments.

 

2.  Adding rand cargo

The next part is to generate a random number, but rust by default doesn't have rand number generator in standard library, so we are going to use a crate or dependency in other words.

If we navigate to rand page, on the right side we will be able to see the version, copy that to the dependency section in your Cargo.toml file. 
The line you added will be something like this rand = "0.8.4", what we are saying here is to use at least version 0.8.4 but not greater than 0.9.0.  This behavior can be seen when we do fresh cargo build in a different PC, but even after the 0.8.5 is released our local branch will not be updated, this is due to Cargo.lock file.  Not only will it cache the version of our dependencies but it also caches the version of our dependencies' dependencies.

If you want to update the version, you can do so by running cargo update, during which your Cargo.lock file will be updated.

Regarding traits, we won't know which one to use, so it's always recommended to read the documentation or run the above command.

Let's add the package for rand by adding this use rand::Rng and let's also add 

let secret_number = rand::thread_rng().gen_range(1..101);

If you notice, we aren't using Rng in this line, while it is weird, apparently, Rng is not really a module, but a "trait" which will be explored in the future chapters, and for us to generate a random number this Rng needs to be in scope, weird.

rand::thread_rng function gives a random number generator, seeded by the OS. 

1..101 means we are specifying a range, lower bound inclusive, upper bound exclusive or we can also use 1..=100

If we want to read the documentation about the crates we are using we can run cargo doc --open and select the crate from the left bar.

You can try printing the random number if you want, now on to the next part, to compare if the input is smaller or greater than the secret number.

 

3.  Comparing 

For this part we need to use another use statement - use std::cmp::Ordering; and add the following code.  Ordering is also similar to Result enum and has the following 3 enum valuies

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Small"),
        Ordering::Greater => println!("Big"),
        Ordering::Equal => println!("GG"),
    }

One way to understand this is to think of a switch statement, and each section is called an arm, if one condition matches, other's won't be compared.

There is one problem here, that we will encounter if we build it now, that is guess is String, but secret_number is an integer.

The reason we didn't explicitly give the type is because rust can understand by looking at the right hand side of the equal operator.

As far as random number there are few types it could've been i32, u32(unsigned) and i64, but rust defaults to i32.

In python or javascript, if we want to change the type of variable, we parse it and assign it to the same variable but that doesn't make sense in, say Java, because when the variable was created we got into a contract saying that I'll pass values that are only of the type I've created, so we end up creating value_string to store the string format.

Rust takes both their sides, and gives us a way to do that - called Shadowing - we can change the variable type, but it should be created anew using let command, so the line will be something like this 

let guess: u32 = guess.trim().parse().expect("Not a valid number");

trim removes whitespace, new line characters, this parse is interesting because it parses to the type needed in the left side of the = operator.

 

4.  Loop and finishing the game

This might be a good place to try to run the program.  Now there is just one more thing left to do, to keep guessing in a loop, it's easy in rust, you just need to enclose the code in brackets with keyword loop, this will cause an infinite loop.

We now need to break the program if we guess it correctly, let's edit the Ordering::Equal arm to like this

            Ordering::Equal => {
                println!("GG");
                break;
            }

 

One minor change we can do is not crash the program when there is an invalid input, in order to do that, we should change the expect while parsing to match like the following:

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => {
                println!("Please enter a valid integer, try again");
                continue;
            }
        };

If you've followed till the end, congrats, your end code should be something like this

use std::io;
use rand::Rng;
use std::cmp::Ordering;

fn main() {
    println!("Guess the number");
    let secret_number = rand::thread_rng().gen_range(1..101);
    loop {
        println!("Your guess?");
        let mut guess = String::new();
        io::stdin()
            .read_line(&mut guess)
            .expect("Failed");
        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => {
                println!("Please enter a valid integer, try again");
                continue;
            }
        };
    
        println!("You guessed: {}", guess);
    
        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Small"),
            Ordering::Greater => println!("Big"),
            Ordering::Equal => {
                println!("GG");
                break;
            }
        }
    }
}

End of Chapter2

 




Tags · Rust, Tech, Blog