Why you should code in Rust in 2021
Romaric P.
Posted on May 6, 2021
Rust is one of the most loved languages by developers for 5 years - writing code in Rust will push you to be a better developer even if you will never use it in your daily job. In this article, I explain why you should learn to write code in Rust. Let's go.
Better understanding of memory management
I remember the time I used to learn to program in Python. Memory was managed by the language, and I didn't need to understand how things work. Grasping algorithmic and all basic concepts when you are a beginner takes time. So spending time thinking about how to manage memory allocation is meaningless. But once you are fluent in programming and it is now so easy that you can focus on business value, it is maybe time to dig into how to build more efficient programs.
For a long time, the efficiency of a program was about lower cost and performance. But in 2021, we can add a third dimension - power efficiency 🌎.
Heap vs Stack
Java, Python, Javascript, Typescript, Go, Haskell, C# are managed programming languages. You don't need to think about memory allocation - does this variable x
is allocated on the heap or on the stack? (read more about Heap vs Stack). On the other hand, programming languages like C, C++, and Rust forced you to think about how you want to memory allocate your variable x
.
# Python - Stack x memory allocation
x = "hello world"
// Rust - Stack x memory allocation
let x: &str = "hello world";
Those two var assignations look similar, right? They are! They are allocated on the Stack. The difference is that with Rust, we have more fine-grained control on the memory allocation. For instance, to allocate "hello world" on the Heap:
// Rust - Heap x memory allocation
let x: String = String::from("hello world");
Heap allocation is not explicitly possible with Python - The interpreter manages it for you.
To summarize: Stack is used for static memory allocation and Heap for dynamic memory allocation, both stored in the computer's RAM. Variables allocated on the stack are stored directly to the memory, and access to this memory is very fast. The Heap memory allocation is slower than the Stack memory allocation.
Reference vs Value
In Python, you don't even need to think about whether your function variables are passed by reference or value.
# declare a function "sum"
def sum(a, b):
return a + b
sum(2, 1) # the result is 3
In this situation, values 2 and 1 are passed to the function sum
by value.
# declare a function "sum" accepting a list object
def sum(elements):
accumulator = 0
for e in elements:
accumulator += e
sum([2, 1]) # the result is 3
And in this situation, the list with values 2 and 1 inside is passed to the function sum
by reference. 🙄
It is not exactly true. In Python, "object references are passed by value" and not by reference. But I will not go into details here.
In Rust, you have to be specific if you want to pass a variable by reference or value.
// pass by value
fn sum(a: u16, b: u16) -> u16 {
a + b
}
sum(2, 1) // the result is 3
Values 2 and 1 are passed to the function by value. To pass by reference, you have to use &
explicitly.
// pass by reference
fn sum(elements: &Vec<u16>) -> u16 {
let mut accumulator = 0;
for e in elements {
accumulator += e
}
accumulator
}
let values = vec![2, 1];
sum(&values) // the result is 3
We pass a list of values by reference to the function sum
. And we can do the same by passing the value of the list by removing &
// pass by reference
fn sum(elements: Vec<u16>) -> u16 {
let mut accumulator = 0;
for e in elements {
accumulator += e
}
accumulator
}
let values = vec![2, 1];
sum(values) // the result is 3
The difference is that Rust will consume the list and will remove it from memory. This is a specific behavior of Rust (read ownership ).
Passing variable by reference or value look anecdotal, but they are not when you need to build highly performant system like databases, embedded devices, and many others.
Build safer programs
As a programmer, when you start to build concurrent programs, you don't really realize how hard it is to write safe concurrent code. This is so abstracted and easy to write unsafe code that we all fall into the trap at least once, and even when we are more experienced. Let me show one unsafe Golang code that I saw one time from an experienced developer:
// golang example
m := make(map[string]string)
go func(m map[string]string) {
m["gender"] = "male"
}(m)
go func(m map[string]string) {
m["gender"] = "female"
}(m)
fmt.Println(m) // m["gender"] is male or female?
this is not the exact code, but the idea of concurrently modifying a hashmap was the same
So, the value of m["gender"]
is male
or female
? 🤔
We can't know. This code is not deterministic due to concurrent writes. If you run this code multiple times, you will sometimes have male
, sometimes female
, and even sometimes fatal concurrent write errors
😨 To concurrently edit the hashmap m
you have to make it thread safe. This kind of error happens all the time, even to experienced developers in more complex situations. This is where Rust helps developers to prevent this kind of mistake. Here is the equivalent in Rust:
let mut m: HashMap<&str, &str> = HashMap::new();
thread::spawn(move || {
m["gender"] = "male";
});
thread::spawn(move || {
m["gender"] = "female";
});
println!("{:?}", m);
And the compiler prevents you from doing nasty stuff.
error[E0382]: use of moved value: `m`
--> src/main.rs:11:19
|
5 | let mut m: HashMap<&str, &str> = HashMap::new();
| ----- move occurs because `m` has type `HashMap<&str, &str>`, which does not implement the `Copy` trait
6 |
7 | thread::spawn(move || {
| ------- value moved into closure here
8 | m["gender"] = "male";
| - variable moved due to use in closure
...
11 | thread::spawn(move || {
| ^^^^^^^ value used here after move
12 | m["gender"] = "female";
| - use occurs due to use in closure
Rust compiler is super smart, it prevents you from doing race condition, and there is no way to compile this kind of code because it is simply wrong. That's why so many developers said that Rust is frustrating. The Rust compiler is just honest with you and tells you the truth about your code. You were probably writing wrong code for long time, and you didn't know until now. I know, it's hard to change habits, and that's why in this kind of situation Rust is your best friend. It will always tell you the truth, even if it is not pleasant :)
A thread-safe code in Rust looks like this (don't use it in production - using .unwrap()
here can block threads):
let mut m: Arc<Mutex<HashMap<&str, &str>>> = Arc::new(Mutex::new(HashMap::new()));
thread::spawn(move || {
m.lock().unwrap()["gender"] = "male";
});
thread::spawn(move || {
m.lock().unwrap()["gender"] = "female";
});
println!("{:?}", m);
Conclusion
I hope you liked this article, and it gives you the appetite to try out Rust. If you have no idea how to start learning it, I would recommend reading the official free ebook. Then, trying to reimplement some good old academic (or not) algorithms and data structures in Rust. If you want to put your hands into dirty stuff, I can recommend contributing to my project Qovery Engine and RedisLess as well.
Here is a shortlist of Rust projects that I recommend to read:
- Meilisearch : Algolia and Elasticsearch search engine alternative.
- Sonic : Lightweight Elasticsearch alternative.
- Sled : Storage engine written in Rust - alternative to RocksDB.
- IsomorphicDB : PostgreSQL clone - it is a good experimental project written in Rust.
- Raft-rs : Raft consensus protocol implemented in Rust by PingCap.
- RedisLess: RedisLess is a fast, lightweight, embedded, and scalable in-memory Key/Value store library compatible with the Redis API.
- Qovery Engine: Qovery Engine is an open-source abstraction layer library that turns easy apps deployment on AWS, GCP, Azure, and other Cloud providers in just a few minutes.
==========
I am hiring Rust developers for my wonderful company.
Posted on May 6, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.