Month of Rust Update 1: First Impressions

As I tweeted about over the weekend, I’m going to be starting a month of deep diving into Rust for the month of May. I’m not trying to be a complete convert. I have work to do after all. However I really want to explore this new lower level language as opposed to my day to day work in managed and interpreted languages. I’m going to try to spend an hour or so each day working through tutorials and maybe trying to build my first real application with it. I’m starting with the Rust website seems to have some great resources like a language guide book and tutorials. I’ll go on from there. Today however was just getting the system setup and working through my first hello world tutorials. So far it’s been a mostly positive experience.

First, the language installation was a snap. It’s literally just as easy as running a shell script and it configures everything for you. It could have been made incrementally easier if they also updated the shell script so it always worked every time after but that’s a minor annoyance. Next it was to get an IDE setup. I decided to go with JetBrains CLion which seems to provide a pretty good experience in terms of build steps, autocomplete, interactions with the package manager (called Cargo), etc. Then it was off to trying the first of the tutorial exercises. This is where the fun begins. This is where I could start to experience the language.

This is not just some syntactical mutation of C or C++, two languages advocates of Rust often look to compare against. It has some constructions which aren’t foreign to any languages per se but the way they are used in Rust is very fascinating to me. You can read the manual if you want and step through the “Guess Game” example found here in the tutorial but I’m going to just post the code and talk about some of the observations I have learned just from this small exercise.

use std::io;
use rand::Rng;
use std::cmp::Ordering;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1, 101);

    println!("Please input your guess. ");

    
    loop {
        let mut guess = String::new();
        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(error) => {
                println!("Invalid Input: {}", error);
                continue; },
        };

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

    }

}

Much of the syntax looks like a lot of new modern languages. Like Kotlin it has a preference for immutable types. Therefore if you don’t specify mut when you define your variable it’s going to be immutable. That certainly makes it stand out more than a one letter difference between val and var in Kotlin and is more like a modern Javascript const and let nomenclature. The real power however comes from the concept of “enum types”. I am not reading ahead so I can only describe my intro chapter rendition of what an enum type is. Essentially it sounds like a C-style union to me. A variable of specified type can be uniquely one of several specific values at one time. So a method that returns a Result type could either have an “OK” result or an “Err” result. That looks very similar to enums in other languages but in Rust it is more than just an integer or string. Because of that it allows us to return various types of return data depending on the execution path in the code. That lets us do some pretty interesting things. For example it allowed us to look at the result of the compare method and determine whether it is greater than less than or equal to using a switch statement. We could have done that with a C#/Java style integer-based enum too of course. However the enum concept let us do the same thing for formatting error handling on the string to integer conversion. This created a very concise and readable code block without dealing with exceptions. Other languages provide some similar shortcuts. In C# we could have done:

if (!Int32.TryParse(guessString, out var guess)) {
    Console.Writeline("Invalid input");
    continue;
}

It’s not that much more text difference in terms of volume of text or code readability but this sort of idiom can be used for error handling in general versus using exceptions which we’d have to do in many cases, such is the C# case using the regular parse:

int guess;
try 
{
    guess = Int32.Parse(guestString);   
}
catch(FormatException e)
{
    Console.Writeline("Invalid input");
    continue;
}

I like the idea of the compactness of the Rust error handling flows. I’m curious how that works out in practice in more complicated programs. I guess we will find out. Embedded in the code is one of the above examples is one of the weirdest things I’ve seen so far in Rust though: intrablock variable shading. We have variable shading in other languages when we have the same variable in different scopes. Take for example this fictitious class:

class MyClass
{
    string text;

    ...

    void AppendText(string text) 
    {
        this.text += text;
    }
}

In Rust however this variable shading is not only allowed but actually an idiom they bring up this early on as an example of how to do things in Rust code. In our rust code we have the following:

let myVariable = "Variable String";
let myVariable = 1234;

Now wait a minute, it seems I just created a variable twice. It seems I created an immutable variable twice even. In practice it’s actually shading the first variable with the second. I get the idea of shading with different blocks of code with different scopes. Any code not in the inner scope would use the outer scope variable naturally and so on. In this case though it seems like the original variable is no longer accessible after the declaration of the second. According to the manual this idiom is used to make more readable code while getting around the fact Rust is a strongly typed language. I’m curious how much this is leveraged and how this idiom works in practice. It was just a fascinating thing I wasn’t expecting.

Now lets talk about two of my minor disappointments: string formatting and executable size. First there is the matter of not having string interpolation for string generation. On string formatting Rust seems to borrow more from C# than C style formatting strings. For fully formatted strings that’s cool but I’ve gotten so used to string interpolation that I’ve found it a nuisance when it’s not available in a language. It is obviously surmountable but hopefully string interpolation is something coming down the pike.

One of the reasons I’m looking at Rust is the idea of having compact and fast code without all the bloat of runtimes and the like for deployment. Yes, Rust requires no runtime, however the executable size for even the literal “Hello World” program was 2.7 MB. A hello world console program in C# is just 114 KB, but of course requiring a whole runtime to be installed to run said 114 KB program. With the whole runtime even paired down I think it turns even the smallest C# program into a 20-80 MB deliverable. So that still gives an advantage to Rust over C# or Java. However it’d be great if it was as compact as a C or C++ program. It’s not a practical problem but I was just surprised how large the simplest of programs are.

So chalk up day one to a very positive Rust experience and I’m looking forward to day two.