The promise of Rust
Thanks to my sponsors: Beth Rennie, Joseph Montanaro, Dylan Anthony, Daniel Silverstone, Sylvie Nightshade, L0r3m1p5um, Mario Fleischhacker, Paul Horn, Sawyer Knoblich, Luke Konopka, Thehbadger, Diego Roig, Matt Jackson, Chris Emery, Malik Bougacha, Samit Basu, ZacJW, Jack Maguire, Stephan Buys, Scott Steele and 267 more
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…
…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.
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);
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.
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.