Sunday, September 26, 2021

Initial Impressions of Rust

I experimented a little with Rust this past week. I haven't gone very deep at this point, but there are a few things I found interesting. To point some of these out, I'm using a number guessing game (details on the sample and where I got it are at the end of the article). The code can be viewed here: https://github.com/jeremybytes/guessing-game-rust.

Disclaimer: these are pretty raw impressions. My opinions are subject to change as I dig in further.

The code is for a "guess the number" game. Here is a completed game (including both input and output):

    Guess the number!
    Please input your guess.
    23
    You guessed: 23
    Too small!
    Please input your guess.
    78
    You guessed: 78
    Too big!
    Please input your guess.
    45
    You guessed: 45
    Too small!
    Please input your guess.
    66
    You guessed: 66
    Too small!
    Please input your guess.
    71
    You guessed: 71
    You win!
So let's get on to some of the language and environment features that I find interesting.

Automatic Git

Cargo is Rust's build system and package manager. The following command will create a new project:

cargo new guessing-game

Part of the project creation is a new Git repository (and a .gitignore file). So there are no excuses about not having source control.

Pattern Matching

Here's a pattern matching sample (from the main.rs file in the repository mentioned above):

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => {
            println!("You win!");
            break;
        },
    }

In this block "guess.cmp(&secret_number)" compares the number the user guessed to the actual number. "cmp" returns an Ordering enumeration. So "Ordering::Less" denotes the "less than" value of the enumeration.

Each "arm" of the match expression has the desired functionality: either printing "Too small!", "Too big!", or "You win!".

As a couple of side notes: the "println!" (with an exclamation point) is a macro that prints to the standard output. I haven't looked into macros yet, so that will be interesting to see how this expands. Also the "break" in the last arm breaks out of the game loop. We won't look at looping in this article.

Error Handling

I like looking at different ways of approaching error handling. Earlier this year, I wrote about the approach that Go takes: Go (golang) Error Handling - A Different Philosophy. Go differs quite a bit from C#. While C# uses exceptions and try/catch blocks, Go uses strings - it's up to the programmer to specifically check for those errors.

Rust takes an approach that is somewhere in between by using a Result enumeration. It is common to return a Result which will provide an "Ok" with the value or an "Err".

Let's look at 2 approaches. For this, we'll look at the part of the program that converts the input value (a string) into a number.

Using "expect"
Let's look at the following code (also from the main.rs file):

    let guess: u32 = guess.trim().parse()
        .expect("invalid string (not a number)");
This code parses the "guess" string and assigns it to the "guess" number (an unsigned 32-bit integer). We'll talk about why there are two things called "guess" in a bit.

The incoming "guess" string is trimmed and then parsed to a number. This is done by stringing functions together. But after the parse, there is another function: expect.

The "parse" function returns a Result enumeration. If we try to assign this directly to the "guess" integer, we will get a type mismatch. The "expect" function does 2 things for us. (1) If "parse" is successful (meaning Result is "Ok"), then it returns the value that we can assign to the variable. (2) If "parse" fails (meaning Result is "Err"), then our message is added to the resulting error.

Here's a sample output:

    Guess the number!
    Please input your guess.
    bad number
    thread 'main' panicked at 'invalid string (not a number): ParseIntError { kind: InvalidDigit }', src\main.rs:19:14
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
    error: process didn't exit successfully: `target\debug\guessing_game.exe` (exit code: 101)
I typed "bad number", and the parsing failed. This tells us that "'main' panicked", which means that we have an error that caused the application to exit. And the message has our string along with the actual parsing error.

Pattern Matching
Another way of dealing with errors is to look at the Result directly. And for this, we can use pattern matching.

Here is the same code as above, but we're handing the error instead:

    let guess: u32 = match guess.trim().parse() {
        Ok(num) => num,
        Err(msg) => {
            println!("{}", msg);
            continue;
        },
    };
Notice that we have a "match" here. This sets up a match expression to use the Result that comes back from the "parse" function. This works similarly to the match expression that we saw above.

If Result is "Ok", then it returns the value from the function ("num") which gets assigned to the "guess" integer.

If Result is "Err", then it prints out the error message and then continues. Here "continue" tells the containing loop to go to its next iteration.

Side note: The "println!" macro uses placeholders. The curly braces within the string are replaced with the value of "msg" when this is printed.

Here is some sample output:

    Guess the number!
    Please input your guess.
    bad number
    invalid digit found in string
    Please input your guess.
    23
    You guessed: 23
    Too small!
This time, when I type "bad number" it prints out the error message and then goes to the next iteration of the loop (which asks for another guess).

Overall, this is an interesting approach to error handling. It is more structured than Go and its error strings but also a lot lighter than C# and its exceptions. I'm looking forward to learning more about this and seeing what works well and where it might be lacking.

Immutability

Another feature of Rust is that variables are immutable by default. Variables must be made explicitly mutable. 

Here is the variable that is used to get the input from the console (from the same main.rs file):

    let mut guess = String::new();

    io::stdin().read_line(&mut guess)
        .expect("failed to read line");
The first line creates a mutable string called "guess". The next line reads from the standard input (the console in this case) and assigns it to the "guess" variable.

A couple more notes: You'll notice the "&" in the "read_line" argument. Rust does have pointers. Also the double colon "::" denotes a static. So "new" is a static function on the "String" type.

Immutable by default is interesting since it forces us into a different mindset where we assume that variable cannot be changed. If we want to be able to change them, we need to be explicit about it.

Variable Shadowing

The last feature that we'll look at today is how we can "shadow" a variable. In the code that we've seen already, there are two "guess" variables:

    let mut guess = String::new();
This is a string, and it is used to hold the value typed in on the console.

    let guess: u32 = guess.trim().parse()
This is a 32-bit unsigned integer. It is used to compare against the actual number that we are trying to guess.

These can have the same name because the second "guess" (the number) "shadows" the first "guess" (the string). This means that after the second "guess" is created, all references to "guess" will refer to the number (not the string).

At first, this seems like it could be confusing. But the explanation that goes along with this code helps it make sense (from The Rust Programming Language, Chapter 2):
We create a variable named guess. But wait, doesn’t the program already have a variable named guess? It does, but Rust allows us to shadow the previous value of guess with a new one. This feature is often used in situations in which you want to convert a value from one type to another type. Shadowing lets us reuse the guess variable name rather than forcing us to create two unique variables, such as guess_str and guess for example. 
I often use intermediate variables in my code -- often to help make debugging easier. Instead of having to come up with unique names for variable that represent the same thing but with different types, I can use the same name.

I'm still a bit on the fence about this. I'm sure that it can be misused (like most language features), but it seems to make a lot of sense in this particular example.

Rust Installation

One other thing I wanted to mention was the installation process on Windows. Rust needs a C++ compiler, so it recommends that you install the "Visual Studio C++ Build Tools". This process was not as straight forward as I would have liked. Microsoft does have a stand-alone installer if you do not have Visual Studio, but it starts up the Visual Studio Installer. I ended up just going to my regular Visual Studio Installer and checking the "C++" workload. I'm sure that this installed way more than I needed (5 GBs worth), but I got it working on both of my day-to-day machines.

Other than the C++ build tools, the rest of the installation was pretty uneventful.

I'm assuming that installation is a bit easier on Unix-y OSes (like Linux and macOS) since a C++ compiler (such as gcc) is usually already installed.

There is also a way to run Rust in the browser: https://play.rust-lang.org/. I haven't played much with this, so I'm not sure what the capabilities and limitations are.

For local editing, I've been using Visual Studio Code with the Rust extension.

Documentation and Resources

The sample code is taken from The Rust Programming Language by Steve Klabnik and Carol Nichols. it is available online (for free) or in printed form.

I have only gone as far as Chapter 2 at this point. I really liked Chapter 2 because it walks through building this guessing game project. With each piece of code, various features were showcased, and I found it to be a really good way to get a quick feel for how the language works and some of the basic paradigms.


Wrap Up

I'm not sure how far I'll go with Rust. There are definitely some interesting concepts (that's why I wrote this article). Recently, I converted one of my applications to Go to get a better feel for the language and stretch my understanding a bit (https://github.com/jeremybytes/digit-display-golang). I may end up doing the same thing with Rust.

Exploring other languages can help us expand our ways of thinking and how we approach different programming tasks. And this is useful whether or not we end up using the language in our day-to-day coding. Keep expanding, and keep learning.

Happy Coding!

1 comment:

  1. Thanks for sharing this! I’m delighted with this information, where such important moments are captured. All the best!

    ReplyDelete