Thoughts on Rust

I finally caved into the pressure from all the smart people I know who have told me for at least the last two years that the Rust programming language is wonderful and we should all jump on it. I finally caved and decided to follow some tutorials over a week and then attempted to build a small program with it. If you do not know my background, I have never coded in C/C++—the closest I did was Turbo Pascal, and later Delphi—but I have mostly worked with memory-managed runtimes for 15 years (.NET, JVM, JS). So caring deeply about memory is a big change for me.

For interest, I did all my coding either in the Rust playground (which is nice) or VSCode, which worked flawlessly.

Before I get to my thoughts on it, I want to call out five interesting aspects that were top of mind for me.

Included tooling

The first thing that jumped out was the lovely tooling, called Cargo, which is included as standard. In theory, you could avoid it and just use the pure Rust language, but literally every tutorial and discussion assumes it is there—and it makes life so much easier. It is beautiful and handy to have a universal tool stack. Cargo does several things: at first, it is a capable package manager, similar to Node or Yarn. In addition, it includes a built-in formatting tool and a linter, which helps reduce bikeshedding around language conventions. This is wonderful. Rust and Cargo also have their own testing framework, lowering barriers and making it so easy to get started. Cargo even includes a documentation generator that takes comments from your code—combined with the code itself—to build very useful docs.

Delightful error messages

When I made mistakes in my code—which happened often as I got to grips with the language—the error messages Rust produced were absolutely beautiful and useful! For example, I tried to use ++ (to do an increment), which does not exist in Rust, and the error message very clearly told me it does not exist and what I could use as an alternative instead.

Standard Error Compiling playground v0.0.1 (/playground)
error: Rust has no postfix increment operator
--> src/main.rs:3:10
   |
3  | hello++;
   |      ^^ not a valid postfix operator
   |
help: use `+= 1` instead
   |
3  | { let tmp = hello; hello += 1; tmp };
   | +++++++++++ ~~~~~~~~~~~~~~~~~~~
3 - hello++;
3 + hello += 1;

Documentation unit tests

Having "built-in" tooling—especially for testing—means you can do amazing things with the knowledge that it is there. Rust does one of the most interesting things I've seen in any language: including unit tests in the documentation. These tests are executable in VSCode and run with Cargo, but they don’t need to be far from the code—it’s awesome. They also end up visible in the documentation that Cargo can generate. It’s brilliant.

Shadowing immutable variables & redeclaring variables

Moving from the good to the bad: redeclaring variables is something Rust supports, and it baffles me that it is even allowed. A small primer on this: variables are immutable by default in Rust—this is great! You can make them mutable if you want, and changing the value of immutable variables is not possible. For example, this fails:

fn main() {
    let aMessage = "This is the initial message";
    aMessage = "this will error"; // Error: cannot assign twice to immutable variable
    println!("{}", aMessage);
}

But we can redeclare the variable—even if it was previously immutable—and I understand some scenarios where this might be useful, but it feels dirty. If this were limited to mutable variables, it would make more sense, but it’s confusing. In the previous example, it prints "this will error," which is at least logical. However, when we add changes to scope, everything goes out the window. It feels like a mess to me, and I hope the linter grows stricter to discourage it.

Passing ownership of variables to functions

This isn’t bad, but it’s easily the biggest shift in thinking required. If you’re coming from C#/Java/JS and you create a variable inside a function and pass it to another function, that variable still exists in the original scope. In Rust, unless you opt-in—and meet other requirements—if you pass a variable to a function, that function owns it, and it ceases to exist in the original scope. Here’s an example of what I mean:

fn writeln(value: String) {}

fn main() {
    let a_message = String::from("Hello World");
    println!("{}", a_message);
    writeln(a_message);
    println!("{}", a_message); // Error: value borrowed here after move
}

It’s confusing at first, but there are solutions, and I do like that Rust encourages better design patterns through this rule.