Reading time: 11 minutes
Welcome to the Rust Tutorial 3!
This will be a short tutorial and is a continuation of the previous tutorial.
In this tutorial, we will handle errors in our guessing game and also learn some more concepts on the way.
So let's get started!
const
keyword
Just like the let
keyword, const
is used to declare variables in Rust that will never be changed through out the program.
One key thing you have to know about const
variables is that, we cannot mutate these variables, even if you use the mut
keyword. Using the mut
keyword in a const
variable will throw an error.
Also, it's a convention in Rust to use upper snake case names for const
variables.
Let's just use an example:
fn main() {
const SOME_NUMBER: i32 = 100_000_000;
}
Fun fact: you can use underscores in an integer if the given number is huge for better readability.
99% of the time, you will be using the let
keyword. So just use that. If you are really sure that your variable doesn't change at all in the main program execution, then you may use const
.
match
case
The match
case statement in Rust is very similar to the switch
case statements you see in other programming languages. A match
case is a clean way of writing multiple if
statement that use the same variable to check for some value. An example looks like this:
fn main() {
let age: i32 = 30;
match age {
1 => println!("One!"),
2 | 3 | 5 | 7 | 11 => println!("This is a prime"),
13..=19 => println!("A teen"),
_ => println!("Just some number"),
}
}
In the match
statement, each value in the LHS of the =>
expression represents the cases of the match
statement, separated by commas. For instance, If age is 1, then we print "One!".
We can chain multiple cases using the pipe |
expression if many values give the same output. You can also use a range of numbers as a case.
In case none of the numbers match, we use underscore _
to define a default case.
match
case with variable assignments
Let's take a look at this program:
fn main() {
let age: i32 = 30;
let res: &str;
match age {
1..=10 => res = "Child",
11..=18 => res = "Teen",
20..=30 => res = "Young Adult",
_ => res = "Adult",
}
println!("{}", res);
}
This program prints out the group type based on age using match
case. But notice that in every case, I am assigning a string to the variable res
. You can see that it seems repetitive.
Well there's a way to make it DRY. Checkout this one:
fn main() {
let age: i32 = 30;
let res: &str = match age {
1..=10 => "Child",
11..=18 => "Teen",
20..=30 => "Young Adult",
_ => "Adult",
};
println!("{}", res); // prints "Young Adult"
}
All I did was declare the res
variable and assign it to the match
case statement based on the age
value.
Seems pretty intuitive. Right?
Block statements and Block scope
There are lot of syntactic sugar in Rust that you wouldn't see in other programming languages. Though all the programming languages has some kind of syntactic sugar, Rust has a unique way of handling them. Block statements are one of them.
Block statements are pretty useful when you want to assign a value to some variable "once" (thus not needing a separate function) but requires some complex calculations to get that value.
A major advantage of block statements is that any variable declared inside the block statement are scoped to that block only. This helps in keeping the program consistent and prevent the program from any kind of scope errors.
Let's take an example:
fn main() {
let age: i32 = 30;
{
let age: i32 = 24;
println!("Block Age: {}", age);
}
println!("Global Age: {}", age);
}
Running the code will give this output:
Block Age: 24
Global Age: 30
You would expect that the age
would change to 24 outside the block due to shadowing, but that's not what happened.
Due to block scope, the age
variable declared inside the block will not affect anything outside the block.
Except, if you are mutating the age
variable. Like this:
fn main() {
let mut age: i32 = 30;
{
age += 24;
println!("Block Age: {}", age);
}
println!("Global Age: {}", age);
}
This will print:
Block Age: 54
Global Age: 54
So, yea. If the variable is being declared as mutable, then block statements can change the variables outside the block scope.
Now let's get to an interesting part.
Assigning block statements to the variables.
Let's write this code:
fn main() {
let some_num: i32 = {
let x: i32 = 45;
let y: i32 = 34;
x + y
};
println!("{}", some_num);
}
In this code:
- I declared a variable called
some_num
. - Then I declared a block statement in which I declared two local variables
x
andy
and assigned some values. - Then I add them
x + y
and assign the sum tosome_num
.
This will print out "79" in the console.
Now, you might be confused with the x + y
term that seems out of place compared to the rest of code. Basically what's happening is that after I add x
and y
, since there's no other code remaining, The block will return this value, i.e. x + y
, and assign it to some_num
.
Also, notice that I didn't put a semi-colon after x + y
. This is because if you used a semi-colon after x + y
then the value cannot get assigned to the variable and will return some empty block.
Some more loops!
Let's get back to the loops! In the previous tutorial, I talked about the loop
expression, the simplest of the loops. Now let's learn the for
and while
loop.
A while
loop in Rust looks like this:
fn main() {
let mut i: i32 = 0;
while i < 5 {
println!("{}", i);
i += 1;
}
}
A for
loop in Rust looks like this:
fn main() {
for i in 1..=5 {
println!("{}", i);
}
}
Notice we have used a range in a for
loop.
Making a Better Guessing Game
In the previous tutorial, we have made our guessing game. But you might have noticed that we can guess as many times as you want and the game wouldn't end.
So let's implement a guess limiter to our game.
To do that, we can simply replace the loop
expression with a while
loop expression. For our game, we can let the player have at maximum 5 guesses.
Here's how the code will look like:
use std::io;
use rand::Rng;
fn main() {
let secret_number: i32 = rand::thread_rng().gen_range(1..101);
let mut i: i32 = 0;
while i < 5 {
println!("Guess {}", i + 1);
println!("Guess the number between 1 and 100: ");
let mut user_input: String = String::new();
io::stdin().read_line(&mut user_input).expect("Some Input Error.");
let user_input: i32 = user_input.trim().parse().expect("Not an integer");
if user_input < secret_number {
println!("Too small!");
} else if user_input > secret_number {
println!("Too big!");
} else {
break;
}
i += 1;
}
if i == 5 {
println!("You have lost! The number was {}!", secret_number);
} else {
println!("You guessed it! It's {}!", secret_number);
}
}
The changes that I made here are:
- I declared a mutable variable
i
with initial value as 0. - I replaced the
loop
with awhile
loop when i < 5 (5 guesses). - I added a print statement to keep track of the guesses.
- Once the
while
loop is over, whether normally or usingbreak
statement, it will go through anif
block (outside thewhile
loop) to see ifi
is equal to 5 or not and then set the print statements for each condition.
You can now play the game and you will notice you will only have five guesses to guess the number.
Now let's play the game, but this time let's input some string instead of an integer as our guess.
Let's run the cargo run
command and see the output:
We get an error since we input a string and not an integer. We have to handle these errors in our code.
Let's take a look at our code:
If you observe, the line of code pointed by the arrow is where we are getting the error. We need to modify this line to handle our errors.
What we can do is, if the user inputs a valid input, then it's all okay. But if the user inputs an invalid input, we can notify the user that it's an invalid input and then restart the loop.
We can do this using match case statement for the user_input
variable.
But what will be the variable which we will match the values to?
That will be the user_input.trim().parse()
.
We know that this function will either return a valid result or an error.
We can verify the return value of the function by using these two enums:
-
Ok()
to see if it's a valid result, and -
Err()
to see if it's an error.
These two enums will be our cases for matching user_input.trim().parse()
and then assign that to the user_input
.
Let's modify that one line of code to this:
let user_input: i32 = match user_input.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("Invalid Input! Try again!");
continue;
},
};
In this code, The first case is when the result is valid. We first check whether the user_input.trim().parse()
returns an integer, say num
. The Ok(num)
will verify if it's a valid result.
The second case is when user_input.trim().parse()
returns an Error object. Since, we will check for any kind of errors, we use an underscore _
inside Err()
to verify for any errors.
Also, notice that I removed expect()
function since we are already handling the errors in a better way.
Now let's run the code and see the output:
As you can notice, I have input some invalid inputs to test the game and it works!
Great job! You have made a better guessing game!
I hope you have learnt a lot in this short tutorial. As I have said, I would have included this in the previous tutorial but that one was getting too long and hence, I decided to write a new one just for this.
In the next tutorial, we will make a simple calculator using Rust.
Until next time, have an awesome day ahead!
GitHub Repo: https://github.com/khairalanam/better-rust-guessing-game
If you like whatever I write here, follow me on Devto and check out my socials:
LinkedIn: https://www.linkedin.com/in/khair-alanam-b27b69221/
Twitter: https://www.twitter.com/khair_alanam
GitHub: https://github.com/khairalanam
Top comments (0)