The promise of Rust

The part that makes Rust scary is the part that makes it unique.

And it’s also what I miss in other programming languages — let me explain!

Rust syntax starts simple.

This function prints a number:

fn show(n: i64) { println!("n = {n}"); }

And this program calls that function — it looks like any C-family language so far, we got parentheses, we got curly brackets, we got, uhh…

Cool bear

…string interpolation isn’t very C-like I guess?

fn show(n: i64) { println!("n = {n}"); } fn main() { let n = 42; show(n); }

Rust move semantics

We can call the show function twice, passing it the same variable n both times, with no issues whatsoever:

fn show(n: i64) { println!("n = {n}"); } fn main() { let n = 42; show(n); show(n); }

However, if we were to change our number to a string instead:

fn show(s: String) { println!("s = {s}"); } fn main() { let s = String::from("hiya"); show(s); show(s); }

Then it wouldn’t work!

And I know decades of poor tooling have taught us to treat the output of compilers as noise we can safely ignore while searching for the actual problem, but, in Rust, the compiler is designed to teach:

rust-is-hard on  main [?] is 📦 v0.1.0 via 🦀 v1.79.0 cargo c -q error[E0382]: use of moved value: `s` --> src/main.rs:8:10 | 6 | let s = String::from("hiya"); | - move occurs because `s` has type `String`, which does not implement the `Copy` trait 7 | show(s); | - value moved here 8 | show(s); | ^ value used here after move |

Here, it’s teaching us about the Copy trait.

i64 does implement Copy, because copying an integer from one register to another is very fast — manipulating numbers like these is something computers are very good at!

That’s why we made computers in the first place!

String, on the other hand, does not implement Copy. The String type specifically refers to a valid UTF-8 sequence stored somewhere on the heap. The heap is a memory area managed by an allocator, which has to keep track of what is allocated where!

When we create a second copy of a String, we first have to ask the allocator to reserve enough space for the copy.

Most of the time, the allocator gets to re-use something that was freed recently, but it can end up calling all the way to the kernel, for example if it needs to map more memory pages.

Because the cost of a heap allocation is so variable, a lot of software tries to avoid doing it at sensitive moments: real-time audio applications don’t do it from the audio thread, games try to avoid or minimize allocations to avoid skipping frames.

And in Rust, if you’re ready to accept that unknown extra cost of “cloning” something, potentially resulting in one or multiple heap allocations, you have to call .clone() explicitly.

And that’s what the compiler suggests here:

note: consider changing this parameter type in function `show` to borrow instead if owning the value isn't necessary --> src/main.rs:1:12 | 1 | fn show(s: String) { | ---- ^^^^^^ this parameter takes ownership of the value | | | in this function help: consider cloning the value if the performance cost is acceptable | 7 | show(s.clone()); | ++++++++ For more information about this error, try `rustc --explain E0382`. error: could not compile `rust-is-hard` (bin "rust-is-hard") due to 1 previous error

And indeed, the following program does work:

fn show(s: String) { println!("s = {s}"); } fn main() { let s = String::from("hiya"); show(s.clone()); show(s); }

The other suggestion was passing the string by reference, also called “borrowing” the string, and works just as well in this case:

// note: taking `&String` is needlessly restrictive, but one thing at a time. fn show(s: &String) { println!("s = {s}"); } fn main() { let s = String::from("hiya"); show(&s); show(&s); }

The difference between these two suggestions only makes sense if you’re used to thinking about memory management: in other words, if you come from non-garbage-collected languages, like C or C++.

However, it is relevant even if you’re coming from JavaScript or Go, where memory safety is a lesser concern.

Amos

Because, you know. cgo isn’t Go, but it still exists. And so do native node.js addons.

JavaScript semantics

JavaScript simply doesn’t have the concept of passing something “by value” or “by reference”.

Primitive types like numbers are passed by value, which means this program passes two different copies of s to the inc function, and ends up printing zero twice.

function inc(s) { s += 1; } let s = 0; console.log(s); inc(s); console.log(s);

If we wanted the inc function to be able to modify, or “mutate”, something we’re passing it, we’d need to put it in an object first, and then pass that object, like so:

function inc(o) { o.s += 1; } let o = { s: 0 }; console.log(o); inc(o); console.log(o);

On the other hand, if we wanted to make sure inc could not mutate something we pass it, even though we’re passing it an object… we’d have limited options.

We could pass inc a clone of our actual object — making sure the original remains untouched:

let bad_deep_clone = (o) => JSON.parse(JSON.stringify(o)); function inc(o) { o.s += 1; } let o = { s: 0 }; console.log(o); inc(bad_deep_clone(o)); console.log(o);

Amos

I personally wouldn’t ship that clone function in production, but many people have!

Or we could freeze the object, which would prevent modifying the object’s properties, adding new ones, etc.

function inc(o) { o.s += 1; } let o = { s: 0 }; console.log(o); inc(Object.freeze(o)); console.log(o);

However, the object remains frozen after the call to inc!

And, if you forget to enable strict mode in browsers by slapping “use strict” at the beginning of your code, any modifications are silently ignored instead of throwing exceptions.

Cool bear

Yes, this is actually how you invoke strict mode

I’m still not entirely sure what object freezing is useful for — I feel like it’s rarely what you want.

Go semantics

The situation isn’t much better in the Go language, another popular option.

The rest of this article is exclusive!

(JavaScript is required to see this. Or maybe my stuff broke)