summaryrefslogtreecommitdiff
path: root/src/content/blogs/rust-is-great.md
blob: 6916867bdf7345f670ebad579ee469d6b54ffe90 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
---
title: "Rust is great... except for the language"
description: ""
date: 2026-08-27
---

For those who know me it might be weird seeing me say "Rust is great", after all, I have been
openly bashing and criticize the language for quite a while now.

While my opinion on Rust hasn't changed, I came to a realization that made me appreciate Rust
for what it has taught me -- which I hope to share in this blog.

## What is Rust?

> Rust is a blazingly fast and memory-efficient language: with no runtime or garbage collector,
> Rust's rich type system and ownership model guarantees memory-safety and thread-safety,
> enabling you to eliminate many classes of bugs at compile-time.

Well... at least that's how the Rust website describes the language, in reality this is mostly
propaganda, it's inaccurate and misleading at best.

A more fair definition might look like:

Rust is a language that restricts the way you are allowed to program, enabling it to make certain
guarantees that allows detecting memory and data corruption at compile time.
Rust doesn't have a garbage collector, making it usually faster than garbage-collected languages.

So, both of these definitions seem to focus on Rust being able to guarantees that usually only
garbage collected languages can make, so how does it do that?

Rust accomplishes that via it's most unique feature: the Ownership Model.

## What is Ownership?

There are 3 rules to ownership:

- Each value in Rust has an owner
- There can only be one owner at a time
- When the owner goes out of scope, the value will be dropped (freed/deallocated)

Assigning a value to a variable, passing it as an argument to a function[^1] or returning it will
result in what's called a "move", where the existing variable gives up it's ownership and passes
it to a new owner.

This mostly applies to types that allocate a resource that needs freeing, which in Rust means any
type that implements the `Drop` trait.
Types that don't implement `Drop` such as integers and other primitives are "trivially copyable"
meaning the value just gets copied instead of "moving" ownership.

Just these rules already make a few nice guarantees:

- Values are only dropped exactly once, preventing double frees
- Values can't be used after being dropped, preventing use-after-free (UAF)
- Values will be automatically dropped, preventing SOME memory leaks.

One issue with this, is that if you pass a few values to a function temporarily, you must return
all of them back, otherwise the caller can no longer use them.

This is fixed by another concept called "borrowing" -- as the the name suggests, instead of giving
up ownership, it allows to "borrow" a _reference_ to a value temporarily, and later return the
reference back to owner.

There are 2 types of references, a read-only (immutable) reference, and a read-write (mutable)
reference. By restricting the programmer to only ever have a single mutable reference OR any
number of immutable references Rust is able to guarantee a bit more:

- Data races can't happen as a value will never be written to while also being read/written by
  something else
- References are always valid (no dangling references)

There is one more concept worth mentioning, but first, I'd like to take this opportunity to shine
a light on one of my biggest issues with the language -- the restrictiveness of it.

While these rules do give us guarantees, they make our lives incredibly tedious, we constantly
have to keep track of what's mutable, and what isn't, and what references do we have to what,
and if we get any of that wrong, we have to stop what we are doing, read a long error message, fix
the bug, and then go back and try to remember what we actually wanted to do - and then again,
every few minutes, it destroys productivity.

And it doesn't even need to be a complex example, even this simple code doesn't compile:
```rust
let my_list = vec![1, 2, 3, 4];
let first: &i32 = my_list[0];
my_list.push(5); // error: can't take mutable reference because immutable reference exists (first)
let last: &i32 = my_list[my_list.len-1];
println!("{first} and {second}");
```

This is a very simple code example that doesn't violate any of the guarantees[^2] Rust makes, yet
Rust does not compile this code. This is because not every valid program would be verifiable by
Rust, there will always be correct programs that Rust will deny.

## Lifetimes