Day 13 — Hello, Rust!

Today 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!