In this episode I get random number and convert strings into more numbers. All this to build a guessing game, using the book "The Rust Programming Language" as a guide.
I'm doing a series of 25 minute sessions where I try to get familiar with the Rust programming language. The blogposts in these series are the notes I took of the lessons learned along the way.
Warning to the reader: As always in this series, these are notes I take as I'm learning. They reflect my current understanding and may be incorrect!
Warning: Unused imports
In the previous episode I added use rand::Rng;
to my code. Currently a warning appears on that line telling me this is an unused Import:
I like this, because it helps you keep only the stuff in the code that you actually need. Of course now it's just temporary, until we start generating random numbers.
Cargo update
Before I continues, I had the idea to do a cargo update
to make sure my dependencies (only rand) were up to date. To my surprise, this added a reference in the Cargo.lock file pointing to the rand
crate version 0.4.2.
The result is that .lock file now references two versions of rand
, I'm not sure why and can't answer it right now.
I would like to find out how to uninstall specific version of crates and so on, so that'll be for another 25 minutes.
Generating a random number
The tutorial continues with us adding the following lines of code to finally generate a random number:
let secret_number = rand::thread_rng().gen_range(1, 101);
println!("The secret number is: {}", secret_number);
Type Inference
Rust uses something called type inference. That means you don't have to specify the type (32-bit number, string, ...) when you declare a variable IF Rust can figure out what the type should be.
Take, for example, this line:
let secret_number = rand::thread_rng().gen_range(1, 101);
The secret_number
variable will be of the type that is returned by the gen_range()
function. In this case it's an u32
, which is a 32-bit unsigned integer.
You can know this by figuring out what type gen_range()
returns. But in Visual Code you can also hover the mouse of the variable and it will tell you the type:
Traits
In the tutorial, I read the following passage:
Next, we add another use line:
use rand::Rng
. The Rng trait defines methods that random number generators implement, and this trait must be in scope for us to use those methods. Chapter 10 will cover traits in detail.
Source: Guessing game tutorial: Generating a random number
And further:
Next, we call the
gen_range
method on the random number generator. This method is defined by the Rng trait that we brought into scope with theuse rand::Rng
statement.
Source: Guessing game tutorial: Generating a random number
From somewhere else I read the following definition on what a "trait" actually is:
A trait is a language feature that tells the Rust compiler about functionality a type must provide.
Source: The Rust Programming Language 1st Edition: Traits
From what I understand, it is very much like Interfaces: a type implements a "trait" so that you know the functions these types will provide. In this case, it is the specific random number generator thread_rnd
that implements a trait that offers the gen_range()
function.
In the book, it is adviced to find out what functionality a trait offers by checking out the crate's documentation.
For the rand crate, you can click on "documentation" while looking at the package on crate.io. Which will lead to this site: docs.rs > rand > 0.3.15 > traits
It mentions the available functions, one of which is thread_rng:
thread_rng: Retrieve the lazily-initialized thread-local random number generator, seeded by the system. Intended to be used in method chaining style, e.g.
thread_rng().gen::<i32>()
.
Source: docs.rs > rand > 0.3.15 > functions
Clicking through on thread_rng
reveales thread_rng
returns the type ThreadRnd
:
pub fn thread_rng() -> ThreadRng
Clicking on ThreadRng
shows the functionality offered by ThreadRng
object. One of which is gen_range
. But its definition is written in a way that is not so easy to understand for me at this moment:
fn gen_range<T: PartialOrd + SampleRange>(&mut self, low: T, high: T) -> T
I think there's a lot to unpack there, especially to understand what <T: PartialOrd + SampleRange>
is about. So I will have to come back to that some other time.
Comparing values
The tutorial continues with the following code to actually check if the guess the user entered was equal, bigger or smaller than the random number:
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small."),
Ordering::Greater => println!("Too big."),
Ordering::Equal => println!("You win!"),
}
Inuitively, one would expect an if
statement or switch
. But in Rust we have the cmp
method - offered by all types that are comparable - of which the result is passed to a match
statement.
cmp
returns an enum value, much like the Result
from previous episodes.
In this case, the match looks for the value Less, Greater or Equal. It doesn't do any more flexible patterns, but I'm sure that's possible. I remember this pattern matching being a highly desired feature for C# and it's now kind of in there.
But as said, in this case the patterns are pure values: three possible things can happen: You win, it's less or it's more.
How did we know cmp()
would be available in the result of gen_range()
? It returns an u32
(32-bit unsigned integer) and the documentation for that type lists that it implements the Ord
trait.
The Ord
trait itself offers the cmp()
function:
fn cmp(&self, other: &Self) -> Ordering;
The Ordering
type is the Enum that we use in our match
.
Converting strings to numbers
One thing we have to do in the guessing game is convert a string to a number.
It offers the following code:
let guess: u32 = guess.trim().parse()
.expect("Please type a number.");
The guess is a String
, which offers a trim()
function:
pub fn trim(&self) -> &str
This method returns again a String which offers the parse()
function:
pub fn parse<F>(&self) -> Result<F, <F as FromStr>::Err>
The definition of this I don't yet fully grasp. But it does return a Result
Enum again, just a different one it seems.
Shadowing
One interesting thing about the parse code is that we seem to "redeclare" or "rebind" the guess
variable.
In Rust, you can do this: you replace the existing variable with a new one that has a different value. It's called "shadowing".
The reason for this is that you don't have to declare two variables, for example: guess_str
and guess
.
This is a feature I wasn't expecting and I haven't seen before.
Annotating variable type
The other interesting thing is let guess: u32
. This says we are annotating which type guess
should be(come).
The documentation for parse()
says the following:
Because parse is so general, it can cause problems with type inference. As such, parse is one of the few times you'll see the syntax affectionately known as the 'turbofish': ::<>. This helps the inference algorithm understand specifically which type you're trying to parse into.
Source: String > Methods > parse
I guess I shouldn't feel too bad the declaration of parse
was difficult to understand. The "turbofish" is not used a lot, according to the documentation, but it is the reason "we can tell it what type to parse into".
I would like to learn more about the turbofish later.
Parse error from expect()
Just to see what it looks like, I made the parser produce an error:
> cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target\debug\guessing_game.exe`
Guess the number!
The secret number is: 86
Please input your guess.
54d
thread 'main' panicked at 'Please type a number.: ParseIntError { kind: InvalidDigit }', libcore\result.rs:945:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.
error: process didn't exit successfully: `target\debug\guessing_game.exe` (exit code: 101)
Conclusion
A boatload of new concepts were encountered, with only a few lines of new code:
- Using a Crate
- Traits
- Type Inference
- Pattern Matching
- Shadowing
- Annotating variables for type inference (superfish)
Personal notes:
I noticed this is a way of learning that suits me personally: finding out things as I start using them instead of reading up on all the concepts without actually putting it in practice. And leaving some pieces temporarily "black boxes" until they shouldn't be anymore.
Also, it takes me about an hour to write a blogpost about my 25 Minutes of Rust.
I got some kind feedback on Twitter about this series, and that motivates me to continue doing these. Thank you!