Day 13 — Hello, Rust!
26 August 2020 · recurse-center TweetToday I started reading The Rust Programming Language book. The first step was to set up Rust. In one curl
command, it installed everything (a compiler, a package manager, a code formatter, and more) I would need to write Rust code! This was an awesome experience, and diametrically opposite to when I was trying to learn C/C++ some years ago. I still don't know how to use third-party packages or format code in C/C++.
$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
info: installing component 'cargo'
info: Defaulting to 500.0 MiB unpack ram
info: installing component 'clippy'
info: installing component 'rust-docs'
12.2 MiB / 12.2 MiB (100 %) 2.9 MiB/s in 3s ETA: 0s
info: installing component 'rust-std'
15.9 MiB / 15.9 MiB (100 %) 10.5 MiB/s in 3s ETA: 0s
info: installing component 'rustc'
47.1 MiB / 47.1 MiB (100 %) 10.0 MiB/s in 4s ETA: 0s
info: installing component 'rustfmt'
info: default toolchain set to 'stable-x86_64-unknown-linux-gnu'
Rust is installed now. Great!
To use all these tools, I added Cargo's bin directory to my PATH
variable.
$ set -U fish_user_paths /home/vinayak/.cargo/bin $fish_user_paths
The best thing was that I could launch the book for my installed Rust edition with this simple command:
$ rustup doc --book
At this point, I stopped following the website, and started using the local version of the book that the previous command opened in the browser! I also installed the Rust extension for VS Code.
Hello, World!
Then I wrote the customary hello world program. The additional step of compiling the code before execution seemed like a lot after 4 years of doing mostly Python. But I guess it's nice that this executable can be run even without having Rust installed, unlike Python. The book also pointed out this comparison and mentioned that everything is a trade-off in language design.
Up until now, I was using rustc
to compile and run code. In the last section of chapter 1, the book mentioned that Cargo
(Rust's build system and package manager) might be a better choice as your project grows. Cargo
can be used to create a new empty project:
$ cargo new hello_cargo
Created binary (application) `hello_cargo` package
Cargo
can also build an executable (cargo build
), or build an executable and run it (cargo run
). There's also cargo check
which comes in handy when you just have to check if your code compiles, and not build the executable. The book mentioned that even though the hello_cargo
project is simple, it uses much of the real tooling you’ll use in your Rust career!
A number guessing game
After that, I followed the tutorial in chapter 2 to write a number guessing game. I learned that Rust variables are immutable by default, and that you can use the mut
keyword to define a mutable variable. I also learned about shadowing which lets you reassign a variable instead of creating a new one.
There's also enums and variants. Result
(a common return type) is an enum with the Ok
and Err
variants, which are returned based on a successful and failed function call. Result
also has an expect
method which can be used to raise an error with a descriptive error message when the return variant is an Err
. If you don’t call expect, the program will compile, but you’ll get a warning which indicates that the program hasn't handled a possible error!
$ cargo build
Compiling guessing_game v0.1.0 (/home/vinayak/dev/rust/guessing_game)
warning: unused `std::result::Result` that must be used
--> src/main.rs:10:5
|
10 | io::stdin().read_line(&mut guess);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_must_use)]` on by default
= note: this `Result` may be an `Err` variant, which should be handled
warning: 1 warning emitted
Finished dev [unoptimized + debuginfo] target(s) in 0.46s
To experiment, I tried to print the guess
variable two times.
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, guess);
}
Coming from Python, I was confused when the above code printed the second variable on a new line.
Guess the number!
Please input your guess.
1
You guessed: 1
1
Later I found out that this was happening because read_line()
adds a \n
to the input when you press Enter, which can be removed with a guess.trim()
. It also removes whitespaces. A thing that still has me confused (from later in the tutorial) is the use of rand::thread_rng()
instead of Rng::thread_rng()
, like we did with io::stdin()
above.
use rand::Rng;
let secret_number = rand::thread_rng().gen_range(1, 101);
//
use std::io;
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
Somewhere in there, I also made Ferris panic with rand::thread_rng().gen_range(1, -101);
!
$ cargo run
thread 'main' panicked at 'Uniform::sample_single called with low >= high', /home/vinayak/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/ macros.rs:13:23
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
And finally I learned about match
! (I'd never used something like this in Python, but looks like I'll soon be able to) A match expression consists of arms. An arm consists of a pattern with the code that should be run if the value given to the match
expression matches the arm.
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
Here Ordering
is another enum like Result, its variants being Less
, Greater
, and Equal
. A match
expression can also be used for error handling, which is pretty neat!
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
If parse()
is able to convert the string into a number, the Result
will be an Ok
variant, which will match the first arm and return the num
value that parse()
returned. And if parse()
fails, it will return an Err
variant, which will match the second arm. The underscore is a catchall value to ignore all errors that parse()
might encounter!
I missed the Python interpreter and using the dir()
function throughout all of this, but the experience with Rust has been great so far!