The Influence of Rust in my Programming

by Christoph Bühler

March 2023

About a year ago, I stumbled across Rust – again. I always regarded Rust as the “peak of programming” because the compiler enforces memory safety and throws some weird error messages. Despite the (supposed) steep learning curve, I decided that it is time to try it out. I started learning Rust and since I’m a “learning by doing” person, I decided to use Rust in my Masters Thesis. After some initial, but predicted, bumps in the road, I got more fluent in the language.

By the time I got confident in writing Rust code, I realized that my way of writing code has changed dramatically. My regard to mutable data and which part of an application or function owns the data has improved. Let me share with you my insights, and with them, a very brief introduction to Rust.

Do not get intimidated by references and pointers

Rust was built as a system level programming language. It allows engineers to do whatever they are doing – but better, faster, stronger.

An example: system level programming is regarded as the magic realm in engineering. This topic hosts loads of pitfalls since memory must be handled manually (among other quirks). Rust was designed to tear down this barrier and provide an ecosystem that allows fast code execution without the fear of memory corruption.

As such, Rust comes with references, pointers, reference-counters, and other scary sounding things. However, do not let these things intimidate you. If you have used a “high-level” language like C#, Java, or JavaScript, you are already familiar with references and pointers. In most languages, complex types (aka objects) are passed by reference, while primitive types (like integers, strings, and such) are passed by value.

By value means that the receiving function gets a copy of the effective value, and you cannot modify the value such that the calling function will notice the change. In contrast, by reference passes a memory address to the function where it finds the complex type. This means both functions share the same instance of the object. Modifying some property of the type will result in the calling function seeing the same change after the function execution.

Rust has a notation for references (the classic: &), and thus, allows you to select if you want an object by reference or by value. Even if the parameters are complex types. You can now explicitly select if you want a clone of your complex object (by value), or if you just require a reference to the object.

Do not fight the compiler

As I started my Rust journey, I was angry at the compiler. It did not let me do what I wanted. Turns out, it was right all along. As soon as I began embracing the messages and my understanding of the language and its concepts grew, I started to love the compiler messages.

Rust has one of the best compiler-errors that I’ve seen in my career as a software engineer. The compiler will throw an error at you, and in the same error message, it will point out what went wrong and how you fix it.

Let me show you an example:

rust
fn inputs() -> Vec<(i32, i32)> {
return vec![(0, 0)];
}
fn main() {
let scores = inputs().iter().map(|(a, b)| {
a + b
});
println!("{}", scores.sum::<i32>());
}

The code above will create a vector with integer tuples. The calling function (main) receives ownership over the vector and immediately gives it away to iter(). Therefore, the temporary value goes out of scope and is deleted from memory (more on that later…). This code will result in the following error:

rust
5 | let scores = inputs().iter().map(|(a, b)| {
| ^^^^^^^^ creates a temporary which is freed while still in use
6 | a + b
7 | });
| - temporary value is freed at the end of this statement
8 | println!("{}", scores.sum::<i32>());
| ------ borrow later used here
help: consider using a `let` binding to create a longer lived value
|
5 ~ let binding = inputs();
6 ~ let scores = binding.iter().map(|(a, b)| {
|
For more information about this error, try `rustc --explain E0716`.

The compiler not only correctly states the problem with the code, but also delivers a solution for the problem. This works with various errors during compilation (like borrowing owned data twice, or needing a reference instead of a value, and so on…). All errors can be explained with the --explain flag.

This led me to a better understanding of such error messages. In other languages, the messages may not be that helpful, but I learned to get around their quirks and parse the correct information that I need to fix my issue. There even exist some tools to help you (for example in TypeScript: https://ts-error-translator.vercel.app/).

Data ownership, mutability, and memory safety are important

The Rust compiler drives a hard bargain. In the section above, the example showed code that is out of scope and not safe to access anymore. The compiler complains about the lifetime and gives you a solution to the issue. Since Rust has no garbage collector, memory is managed “manually”. However, the language does manage memory for you, no need to manage it on your own like in C or C++.

In conjunction with memory safety (and the lifetime of data), another core concept that makes Rust an interesting language to learn is data ownership and mutability. In rust, every variable (i.e., data) is “write once, read many”. This disallows multiple writable access to data and prevents side effects in general.

rust
fn take_a_copy(data: String) { /* do something */ }
fn take_a_read_only_reference(data: &str) { /* do something */ }

The two functions above have two distinct types of data ownership (or the take-over thereof). The first function takes the string data by value. The caller of this function is required to create a new String or pass the ownership to the function completely. If the ownership is given to the function, it can be returned, but the callee is not required and may keep the ownership. The second function requires a reference to a string, but only with read access. This allows the data to be “borrowed” but the ownership still lies with the caller.

rust
fn main() {
let the_str: String = "Hello, world!".to_string();
take_a_copy(the_str);
take_a_copy(the_str); // <-- This will produce a compiler error.
}

The compiler will complain:

rust
use of moved value: `the_str`
value used here after move

The ownership was taken by the take_a_copy and further usage of the same variable will produce the error. Even further “borrowing” of the data is not possible.

However, creating a clone will work since it creates a fresh new copy of the data and directly passes the ownership to the callee. Another option is to use read-only references:

rust
fn main() {
let the_ref: &str = "Hello, world!";
let the_str: String = "Hello, world!".to_string();
take_a_copy(the_str.clone());
take_a_copy(the_str.clone());
take_a_read_only_reference(&the_str);
take_a_read_only_reference(&the_str);
take_a_read_only_reference(&the_ref);
take_a_read_only_reference(&the_ref);
}

The code above will compile fine since the take_a_copy function receives a clone in both cases and the take_a_read_only_reference function only has read access to a reference to the data (i.e., a “read borrow”).

This concept has drastically improved my way of thinking about data and their mutability. I adjusted my code style in other languages to comply with the ownership rules of Rust. I create copies of data where it is needed and pass “by reference” (default in many languages when using complex types) but take care to allow write access only once at a time. This concept helps mitigate side effects on your data in an elegant way. To further read about the data ownership topic, head over to the Rust book: What is Ownership? and References and Borrowing.

Conclusions

Rust has changed my personal view of code in a positive way. It does not matter if one uses the language in a work environment or not. Despite the learning curve, it is a beneficial language to learn.

Especially the concepts about data ownership and lifetimes had it on me. I often find myself thinking about the data ownership now and adjusting the code accordingly. After changing some code, most of the time it gets even more readable and understandable.

The perfect place to start learning is the rust programming language book: https://doc.rust-lang.org/book/. The book is well structured and seasoned developers can skip the parts where variables are introduced. However, I personally recommend reading the chapters about ownership, traits, and lifetimes very carefully.

Previous post
Back to overview
Next post