What Makes Rust Amazing?

I’ve been thinking about this for a while, and I think it’s finally time to put these thoughts into a blog post. Will update this later to fill out missing sections but right now I just gotta put these thoughts down while I have some time.

Overview

Note I have organized these in decreasing importance, at least in my view.

References and Ownership

This is the biggest thing ever. Like the other stuff I mention later on is really important too, but without this you don’t have Rust, you have something like Zig or D which while still very good, simply just can’t provide the same guarantees that Rust does in terms of memory safety.

A simple explanation of references in Rust: there are two types:

  • &T, an immutable a.k.a. shared reference
  • &mut T, a mutable a.k.a. exclusive reference

Coming from C++, the analogues are const T& and T&, and coming from pretty much any other language the analogues are… kind of nothing. If you have a reference to an object in Python, Java, Javascript, Go, etc., it’s a mutable one, with kind of um no guarantees around really really classic problems like thread safety and mutable aliasing.

A classic example of mutable aliasing, a.k.a reference invalidation, is the following:

#include <iostream>
#include <vector>

int main(int argc, char** argv) {
    // Create a vector
    std::vector<int> x = {1, 2, 3, 4};
    // Get a reference to the second element in the vector
    const auto& y = x[1];
    std::cout << y << std::endl;
    // Mutate the vector, invalidating `y`
    x.push_back(5);
    std::cout << x[4] << std::endl;
    // ***Insta-UB***
    std::cout << y << std::endl;
}

The funny thing, the absolutely hilarious thing is that, at the time of this writing, no modern C++ compiler (gcc, clang, MSVC) catches this in their standard compilation pass, even with all warnings enabled. The equivalent Rust code:

fn main() {
    let mut x = vec![1, 2, 3, 4];
    let y = &x[1];
    println!("{}", y);
    x.push(5);
    println!("{}", x[4]);
    println!("{}", y);
}

shows a very readable error messages explaining exactly how You Messed Up:

error[E0502]: cannot borrow `x` as mutable because it is also borrowed as immutable
 --> src/main.rs:5:5
  |
3 |     let y = &x[1];
  |              - immutable borrow occurs here
4 |     println!("{}", y);
5 |     x.push(5);
  |     ^^^^^^^^^ mutable borrow occurs here
6 |     println!("{}", x[4]);
7 |     println!("{}", y);
  |                    - immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `playground` due to previous error

This is huge and prevents whole classes of memory errors simply by enforcing mutation to only be done by one “section” of code at a time.

Lifetimes

How this gets enforced is with lifetimes. A lifetime can be thought of as “how long a reference is good for”. In the above example, we got an error because we had a shared reference’s lifetime overlap with that of an exclusive reference.

Lifetimes are also useful because the maximum amount of time a lifetime can be valid for is the lifespan of an object, from when it is created (think malloc, but you can also have stack-only objects) to when it is dropped (think free or popping the stack frame).

Real Move Semantics

Ownership is the other mechanism that makes this possible. I distinguish Rust’s ownership and move semantics from C++‘s icky thing where a """moved""" object is still valid but """empty""" somehow because this is the Real Deal; once you move a Value (data in memory) out of a Variable (label in the program), that Variable is no longer valid. To even do a move in the first place, the compiler must guarantee that there are no outstanding references to that variable.

This mechanism allows the compiler to automatically insert calls to drop values when it detects a variable with ownership cannot possibly be used again. Again, this entire system is just a really neat way for the compiler to statically guarantee exclusive mutation of objects, which is much more complicated but just as important as doing bounds checks on all accesses.

TODO: explain this better

Good Defaults and Good Tooling

TODO

Culture of Safety

TODO

Algebraic Datatypes

TODO

Traits Instead of Objects

TODO


So yeah sorry it’s unfinished and a bit rambly, I promise I’ll get around to the rest of it eventually, at some point, later idk