I finally caved into the pressure from all the smart people I know who have told me for at least the last 2 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 a memory-managed runtime for 15 years (.NET, JVM, JS) and so caring about memory deeply this 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 5 interesting aspects which were front 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 do the pure Rust language but literally every tutorial and discussion assumes it is there, and it makes life so easy. 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/Yarn package manager. In addition, it has a formatting tool and a linter built in as well. This ensures that the bikeshedding around the language is nerfed and that is wonderful. Rust/Cargo also has its own testing framework. It lowers barriers and makes it so easy to get started. Cargo also has a documentation generator which takes the comments from your code combined with your code to build very useful docs.
Delightful error messages
When I screwed up the code, which happened often as I got to grips with the language, the error messages it raised were absolutely beautiful and useful! For example, I tried to do ++ (to do 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.
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 knowing it is there and Rust does one of the most interesting things I’ve seen in any language: include unit tests in the documentation. The tests are executable in VSCode and they run with Cargo but they do not need to be far away from the code - it is awesome from that side and they end up visible in the documentation that Cargo can build too. It is brilliant.
Shadowing immutable variables & redeclaring variables
Moving from the good to looking at the bad; redeclaring variables is something supported and it baffles my mind that it is even allowed. A small primer on this. Variables by default are immutable - 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";
println!("{}", aMessage);
}
But we can redeclare the variable even if it is immutable, and I understand some scenarios it will be useful but it feels dirty.
If this was limited to mutable variables it would make sense, but 🤷♂️ In the above example, it prints “this will error” which is at least logical but when we add changes to scope, all that goes out the window. It feels like a mess to me and I hope the linter gets more strict on not allowing it.
Passing ownership of variables to functions
This is not bad, but it is easily the biggest shift in thinking needed. If you can grok this, you will be ok in Rust. In C#/Java/JS if you are in a function, create a variable and pass said variable to another function… that variable still exists in the original function. Passing by reference or pointer or anything… it does not matter.
In Rust, unless you opt-in (and meet some other requirements) if you pass a variable to a function - that function owns it and it goes away from the original function. Here is an example of what I am talking about:
fn writeln(value: String) {}
fn main() {
let a_message = String::from("Hello World");
println!("{}", a_message);
writeln(a_message);
println!("{}", a_message); // this line will error cause `writeln` now owns `a_message`
}
It is confusing initially but there are solutions and I do like that helps push the right design patterns.
Overall thoughts
Rust is excellent. It is modern and smart. It makes a lot of sense for high-performance systems or where you need to be running on bare metal. I think the intentional limits will make it stay a specialised tool, compared to something like Kotlin or TypeScript which lets you mix & match FP/OOP/shitty code styles.